Menu
CoddyTech

Manejo de errores

Parte de la sección Conceptos básicos de Express del Journey de Node.js de Coddy. Lección 18 de 23.

Cuando una ruta genera un error, Express responde con su propia página de error, que no es JSON y muestra detalles pensados para ti.

app.get('/api/fines/:member', (req, res) => {
    throw new Error('Fines database is down');
});

app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).json({ error: 'Try again later' });
});

Un middleware con cuatro argumentos, (err, req, res, next), es un gestor de errores. Express cuenta los argumentos: con tres, es un middleware normal y los errores lo omiten. Cuando una ruta genera un error, Express omite el middleware normal y llama al gestor de errores con el error. Añádelo al final, después del gestor 404. Registra los detalles para ti; envía al cliente un mensaje breve.

throw new Error('text') detiene la ruta y entrega el error a Express; llamar a next(err) hace lo mismo. Responde con 400 cuando el visitante haya enviado algo incorrecto que pueda corregir. Lanza un error cuando la culpa sea del servidor.

challenge icon

Desafío

Fácil

Continúa con Recipe Box. Puedes ignorar los archivos separados de practice-errors/; Run ahora inicia el archivo server.js del proyecto.

Quienes cocinan quieren ajustar las cantidades de una receta, por ejemplo, duplicar las porciones para una fiesta. Una receta guardada sin porciones no se puede ajustar. Eso es un fallo en tus datos, no algo que quien visita el sitio pueda solucionar, así que se envía a un controlador de errores que responde con un JSON 500 limpio.

  1. En server.js, encima del controlador 404, agrega una ruta para /api/recipes/:id/scale. Lee el factor de la cadena de consulta, como en /api/recipes/1/scale?by=2.
  2. Si no hay ninguna receta con ese id, responde con el estado 404 y {"error": "No such recipe"}.
  3. Convierte by en un número. Si no es un número, o es 0 o menos, responde con el estado 400 y {"error": "Scale by a positive number"}. Number('lots') es NaN, y Number.isNaN lo detecta.
  4. Si la receta no tiene servings, lanza un nuevo Error cuyo mensaje sea Recipe 5 has no servings, reemplazando 5 por el id de esa receta.
  5. De lo contrario, responde con un JSON que contenga solo el título y las porciones ajustadas. La receta 1, Tomato Soup, tiene 4 porciones, así que /api/recipes/1/scale?by=2 envía {"title": "Tomato Soup", "servings": 8}. No agregues ningún otro campo.
  6. Al final del middleware del archivo, después del controlador 404, agrega un controlador de errores: un app.use cuya función reciba cuatro argumentos, (err, req, res, next). Imprime el error con console.error y responde con el estado 500 y {"error": "Something went wrong"}.

Pruébalo: Presiona Run y envía /api/recipes/1/scale?by=2. Deberías ver {"title":"Tomato Soup","servings":8}. Envía /api/recipes/2/scale?by=lots: estado 400 y {"error":"Scale by a positive number"}. Envía /api/recipes/99/scale?by=2: estado 404. Ahora selecciona POST y envía {"title": "Toast"} a /api/recipes. Se convierte en la receta 5, sin porciones. Envía /api/recipes/5/scale?by=2: estado 500 y {"error":"Something went wrong"}, y la consola muestra el error Recipe 5 has no servings.

Pruébalo tú mismo

import { escapeHtml } from './html.js';
import express from 'express';
import { randomUUID } from 'node:crypto';
import { recipes } from './data.js';

const app = express();

app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
});

app.use((req, res, next) => {
    res.set('X-Request-Id', randomUUID());
    next();
});

app.use(express.json());

app.use(express.static('public'));

app.get('/', (req, res) => {
    const items = recipes.map((r) => `<li><a href="/recipes/${r.id}">${escapeHtml(r.title)}</a></li>`);
    res.send(`<link rel="stylesheet" href="/style.css"><h1>Recipe Box</h1><ul>${items.join('')}</ul>`);
});

// type('text') envía texto sin formato, por lo que cada título aparece en su propia línea.
app.get('/recipes', (req, res) => {
    const { tag } = req.query;
    const list = tag ? recipes.filter((recipe) => recipe.tags.includes(tag)) : recipes;
    res.type('text').send(list.map((recipe) => recipe.title).join('\n'));
});

app.get('/recipes/:id', (req, res) => {
    const recipe = recipes.find((r) => r.id === Number(req.params.id));
    if (!recipe) {
        res.status(404).send('No such recipe');
        return;
    }
    res.send(recipe.title);
});

app.get('/api/recipes', (req, res) => {
    res.json(recipes);
});

app.get('/api/recipes/:id', (req, res) => {
    const recipe = recipes.find((r) => r.id === Number(req.params.id));
    if (!recipe) {
        res.status(404).json({ error: 'No such recipe' });
        return;
    }
    res.json(recipe);
});

app.post('/api/recipes', (req, res) => {
    const { title, tags } = req.body;
    if (!title) {
        res.status(400).json({ error: 'A recipe needs a title' });
        return;
    }
    const recipe = { id: recipes.length + 1, title, tags: tags ?? [] };
    recipes.push(recipe);
    res.status(201).json(recipe);
});

app.get('/about', (req, res) => {
    res.send('Recipe Box collects family recipes.');
});

app.get('/menu', (req, res) => {
    res.redirect(301, '/recipes');
});

app.get('/menu/:id', (req, res) => {
    res.redirect(301, `/recipes/${req.params.id}`);
});

app.use((req, res) => {
    if (req.path.startsWith('/api/')) {
        res.status(404).json({ error: 'Not found' });
        return;
    }
    res.status(404).send('<h1>Page not found</h1><p><a href="/">Back to Recipe Box</a></p>');
});

app.listen(3000, () => {
    console.log('Recipe Box is running on port 3000');
});
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Conceptos básicos de Express

Practica por tu cuenta: Playground de Express