Learning Goals
3 minBy the end of this lesson you will be able to:
- Return a consistent error shape like
{ error: "message" }with the right status code. - Add a 404 catch-all after your routes so unknown paths reply with a tidy JSON error.
- Register one error-handling middleware with the four-argument
(err, req, res, next)signature, last of all. - Forward an error from a route to that central handler by calling
next(err).
Warm-Up · What Happens When It Throws?
5 minLast lesson you validated input with Zod, rejecting bad data before it reached your logic. Today we handle what happens when something goes wrong anyway.
Read this route. There is no try/catch and no error middleware. Predict what the browser sees when you visit /menu:
app.get('/menu', (req, res) => {
throw new Error('Database is down')
res.json({ ok: true })
})Reveal
The route never sends a reply of its own. Express catches the thrown error for you and responds with a default 500 Internal Server Error page — often a full stack trace. The visitor gets an ugly wall of text, not tidy JSON. Worse, if the error happened inside an async function with no try/catch, the request can simply hang. Today we replace both outcomes with one clean, consistent error.
New Concept · One Safety Net Under Everything
12 minPicture a trapeze act. High above the ring, acrobats swing and sometimes miss. Under them is one safety net, stretched across the whole floor. Whoever falls, wherever they fall, the same net catches them. A central error handler is that net for your API: one place that catches every fall, so nothing crashes to the floor.
Express has a special kind of middleware just for this. Normal middleware takes three arguments, (req, res, next). An error-handling middleware takes four: (err, req, res, next). That extra first argument, err, is how Express tells it apart from the rest.
The syntax
You register the central handler last, after all your routes. It reads a status off the error, or falls back to 500, and always replies with the same JSON shape:
// central error handler — must be LAST, and take 4 args
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message })
})Just above it, a 404 catch-all handles any request that matched no route at all. It has no path, so it runs for everything that fell through:
// 404 catch-all — runs when no route matched
app.use((req, res) => {
res.status(404).json({ error: 'Not found' })
})Why it matters
A predictable error shape makes the whole system easier to work with. You can debug faster, because every failure looks the same in the terminal. The front-end can handle failures once, reading error from the body instead of guessing. One net, and every part of the app relaxes.
Worked Example · Wrapping the Notes API in a Net
12 minFollow along in your Express project. Priya's notes API works on a good day, but replies with messy stack traces when anything breaks. Let us give it a proper safety net.
Step 1 — Add a 404 catch-all after all routes
Place this below every app.get, app.post and friend. It has no path, so it only runs when nothing above it matched:
// 404 — nothing above matched this request
app.use((req, res) => {
res.status(404).json({ error: 'Not found' })
})Step 2 — Add the central error handler at the very end
After the 404, add the four-argument handler. It is the last thing you register, so it catches errors from anywhere above:
// central error handler — the safety net, always LAST
app.use((err, req, res, next) => {
console.error(err.message) // log it for yourself
res.status(err.status || 500).json({ error: err.message })
})Step 3 — Make a route hand its error to the net
This route looks for one note. If the id is unknown, it builds an error, tags it with a status, and calls next(err) instead of replying itself:
app.get('/notes/:id', (req, res, next) => {
const note = notes.find((n) => n.id === req.params.id)
if (!note) {
const err = new Error('Note not found')
err.status = 404
return next(err) // hand it to the safety net
}
res.json(note)
})Visiting /notes/999 when no such note exists replies:
404 { "error": "Note not found" }And an unknown path like /nope hits the 404 catch-all:
404 { "error": "Not found" }Step 4 — Mind the order
Order is everything here. Routes come first, then the 404 catch-all, then the error handler last:
// 1. all your routes ...
// 2. app.use((req, res) => { ... 404 ... })
// 3. app.use((err, req, res, next) => { ... error ... })What changed? Every failure now leaves through the same door — a consistent { error } body with a matching status — instead of a random stack trace.
Try It Yourself
13 minThree tasks, all in your server.js. Restart the server with node server.js after each change and test in your browser.
Task 1 — Add a 404 handler
Below all of your routes, add a catch-all that replies with a
404status and the body{ error: 'Not found' }:app.use((req, res) => { res.status(404).json({ error: 'Not found' }) })Visit a path you know does not exist, such as
/banana, and confirm you get the tidy JSON error.Task 2 — Add the central error handler
After the 404, register the four-argument handler. Log the message for yourself, then reply with the error's status or
500:app.use((err, req, res, next) => { console.error(err.message) res.status(err.status || 500).json({ error: err.message }) })Keep it as the very last
app.usein the file.Task 3 — Throw a custom status
Add a route that builds an error, sets a
.statusof your choice (try403for “forbidden”), and callsnext(err). Confirm your central handler uses that exact status, not the default500. Change the number and check the response status changes with it.
Mini-Challenge · The Missing Student
8 minCombine today's error handler with the route parameters and status codes you met earlier. Build a GET /students/:id route that looks up one student. When the id matches nobody, it must not reply inline — it should create an error with status 404 and forward it with next(err) to the central handler.
It works if: a known id returns the student as JSON, while an unknown id returns 404 with { error: 'Student not found' } — produced by your central handler, not the route.
Reveal a sample answer
const students = [
{ id: '1', name: 'Wei Jie' },
{ id: '2', name: 'Aisyah' },
]
app.get('/students/:id', (req, res, next) => {
const student = students.find((s) => s.id === req.params.id)
if (!student) {
const err = new Error('Student not found')
err.status = 404
return next(err) // let the safety net answer
}
res.json(student)
})
// central handler catches the forwarded error
app.use((err, req, res, next) => {
res.status(err.status || 500).json({ error: err.message })
})Visiting /students/2 returns:
200 { "id": "2", "name": "Aisyah" }Visiting /students/99 returns:
404 { "error": "Student not found" }Notice the return before next(err). It stops the route right there, so the handler at the end sends the one and only reply.
Recap
3 min- An error-handling middleware takes four arguments:
(err, req, res, next). - Register it last, after every route and after the 404 catch-all.
- A 404 handler is a pathless
app.usethat replies when no route matched. - Call
next(err)to forward an error to the central handler instead of replying inline. - Reply with a consistent shape like
{ error: message }and the right status.
New words
- Error-handling middleware — a four-argument
(err, req, res, next)function that catches errors. - 404 handler — a pathless catch-all that replies when no route matched the request.
- next(err) — passing an error to
nextto send it straight to the central handler. - 500 — the default “Internal Server Error” status when something unexpected breaks.
Homework
4 min to brief · ~20 min to doRequired
- Add both a 404 catch-all and a central error-handling middleware to your API, in that order, at the end of
server.js. Trigger an error — visit an unknown path, or a route that callsnext(err)— then take a screenshot of the JSON error response and bring it to next lesson.
Optional stretch
- Write two sentences: why should error responses not leak a full stack trace in production? Think about what an attacker could learn from your file paths and library names, and what a real visitor actually needs to see.