Menu
CoddyTech

Error Handling

Part of the Express Basics section of Coddy's Node.js journey. Lesson 18 of 23.

When a route throws, Express answers with its own error page, which is not JSON and shows details meant for you.

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' });
});

A middleware with four arguments, (err, req, res, next), is an error handler. Express counts the arguments: with three it is ordinary middleware, and errors skip it. When a route throws, Express skips the ordinary middleware and calls the error handler with the error. Add it last, after the 404 handler. Log the details for yourself; send the client a short message.

throw new Error('text') stops the route and hands the error to Express, and calling next(err) does the same. Respond with 400 when the visitor sent something wrong and can fix it. Throw when the server is at fault.

challenge icon

Challenge

Easy

Continue Recipe Box. You can ignore the separate practice-errors/ files; Run now starts the project's server.js.

Cooks want to scale a recipe, for example twice the servings for a party. A recipe saved without servings cannot be scaled. That is a fault in your data, not something the visitor can fix, so it goes to an error handler that sends a clean JSON 500.

  1. In server.js, above the 404 handler, add a route for /api/recipes/:id/scale. It reads the factor from the query string, as in /api/recipes/1/scale?by=2.
  2. When no recipe has that id, respond with status 404 and {"error": "No such recipe"}.
  3. Turn by into a number. When it is not a number, or it is 0 or less, respond with status 400 and {"error": "Scale by a positive number"}. Number('lots') is NaN, and Number.isNaN spots it.
  4. When the recipe has no servings, throw a new Error whose message is Recipe 5 has no servings, with that recipe's id in place of 5.
  5. Otherwise, respond with JSON that holds only the title and the scaled servings. Recipe 1, Tomato Soup, has 4 servings, so /api/recipes/1/scale?by=2 sends {"title": "Tomato Soup", "servings": 8}. Add no other fields.
  6. At the end of the file's middleware, after the 404 handler, add an error handler: an app.use whose function takes four arguments, (err, req, res, next). It prints the error with console.error and responds with status 500 and {"error": "Something went wrong"}.

Try it: Press Run and send /api/recipes/1/scale?by=2. You should see {"title":"Tomato Soup","servings":8}. Send /api/recipes/2/scale?by=lots: status 400 and {"error":"Scale by a positive number"}. Send /api/recipes/99/scale?by=2: status 404. Now pick POST and send {"title": "Toast"} to /api/recipes. It becomes recipe 5, with no servings. Send /api/recipes/5/scale?by=2: status 500 and {"error":"Something went wrong"}, and the Console shows the error Recipe 5 has no servings.

Try it yourself

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') sends plain text, so each title shows on its own line.
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 iconTest yourself

This lesson includes a short quiz. Start the lesson to answer it and track your progress.

All lessons in Express Basics

Practice on your own: Express playground