Error Handling¶
Your stack trace in the response is a free info leak. Error handling that doesn't leak is not optional
Default error handlers in Express will tell an attacker your file structure , framework version , and package paths. Every error that reaches the client is intelligence. Structured error responses with consistent formats , sanitized messages , and proper status codes turn a liability into a contract. Handle errors globally. Never trust your route handlers to clean up after themselves
the 4-argument error handler¶
Express identifies error-handling middleware by its 4 parameters:
app.use((err , req , res , next) => {
// this only runs when next(err) is called or a handler throws
console.error(err.stack)
res.status(500).json({ error: 'Internal server error' })
})
Express checks Function.length of your middleware. If it sees exactly 4 parameters , it treats it as an error handler. This means the parameter names don't matter - only the count matters
must be last in middleware chain¶
// GOOD - error handler is last
app.use(helmet())
app.use(express.json())
app.use('/api' , routes)
app.use(notFoundHandler) // 404 handler
app.use(errorHandler) // 500 handler - LAST
// BAD - error handler before routes
app.use(helmet())
app.use(errorHandler) // runs before routes - catches nothing
app.use(express.json())
app.use('/api' , routes)
404 handler¶
Express doesn't have a built-in 404 handler. You write one yourself:
app.use((req , res , next) => {
res.status(404).json({
error: 'Not found',
path: req.path
})
})
This must be defined AFTER all routes. If a request doesn't match any route , it falls through to this handler
async error wrapping¶
Here's the big one. Express does NOT catch promise rejections
// THIS CRASHES THE SERVER
app.get('/data' , async (req , res) => {
const data = await db.query('SELECT * FROM users')
// if db.query throws , Express doesn't catch it
// the promise rejection is unhandled
res.json(data)
})
Fix it with express-async-errors:
npm install express-async-errors
// at the VERY TOP of your entry file - before any routes
require('express-async-errors')
const express = require('express')
// ... rest of your app
This patches Express to catch promise rejections and forward them to your error handler
Or wrap each async handler manually:
const asyncHandler = (fn) => (req , res , next) => {
Promise.resolve(fn(req , res , next)).catch(next)
}
app.get('/data' , asyncHandler(async (req , res) => {
const data = await db.query('SELECT * FROM users')
res.json(data)
}))
The express-async-errors approach is cleaner. Don't forget to install it - you will
custom error classes¶
// src/utils/errors.js
class AppError extends Error {
constructor(message , statusCode) {
super(message)
this.statusCode = statusCode
this.isOperational = true // distinguishes expected errors from bugs
Error.captureStackTrace(this , this.constructor)
}
}
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found` , 404)
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message , 401)
}
}
class ValidationError extends AppError {
constructor(errors) {
super('Validation failed' , 400)
this.errors = errors
}
}
module.exports = { AppError , NotFoundError , UnauthorizedError , ValidationError }
Usage in route handlers:
const { NotFoundError } = require('../utils/errors')
app.get('/users/:id' , async (req , res) => {
const user = await db.findUser(req.params.id)
if (!user) {
throw new NotFoundError('User')
}
res.json(user)
})
comprehensive error handler¶
const { AppError } = require('./utils/errors')
app.use((err , req , res , next) => {
// log the full error for debugging
console.error(err)
// set default status
const statusCode = err.statusCode || 500
const message = err.isOperational
? err.message
: 'Internal server error'
// don't leak stack traces in production
const response = {
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
// add validation errors if present
if (err.errors) {
response.errors = err.errors
}
res.status(statusCode).json(response)
})
error handling checklist¶
// 1. Install express-async-errors at entry point
require('express-async-errors')
// 2. Define custom error classes
// AppError , NotFoundError , ValidationError , etc
// 3. Throw errors in handlers
app.get('/:id' , async (req , res) => {
const item = await db.findById(req.params.id)
if (!item) throw new NotFoundError()
res.json(item)
})
// 4. Global error handler - LAST
app.use((err , req , res , next) => {
// sanitize , log , respond
})
// 5. 404 handler - BEFORE error handler
app.use((req , res) => {
res.status(404).json({ error: 'Not found' })
})
prerequisites¶
express_05_static.md - static file serving , caching , security
next → express_07_templating.md