Error Handling Middleware
Unhandled errors in route handlers crash your server or return confusing responses. Express has a built-in error handling mechanism that centralizes all error responses in one place.
The error middleware signature
Section titled “The error middleware signature”Express identifies error handling middleware by its 4-argument signature: (err, req, res, next):
import { Request, Response, NextFunction } from 'express'
function errorHandler( err: Error, req: Request, res: Response, next: NextFunction) { console.error(err.stack) res.status(500).json({ error: 'Internal server error' })}
// Must be registered LAST — after all routes and other middlewareapp.use(errorHandler)Triggering the error handler
Section titled “Triggering the error handler”Call next(error) from any route handler or middleware to skip to the error handler:
app.get('/posts/:id', (req, res, next) => { try { const post = db.prepare('SELECT * FROM posts WHERE id = ?') .get(parseInt(req.params.id)) if (!post) return res.status(404).json({ error: 'Not found' }) res.json(post) } catch (err) { next(err) // passes to the error handler }})A typed AppError class
Section titled “A typed AppError class”Create a custom error class to carry HTTP status codes:
export class AppError extends Error { constructor( public statusCode: number, message: string ) { super(message) this.name = 'AppError' }}Throw it from route handlers:
import { AppError } from '../errors.js'
app.get('/posts/:id', (req, res, next) => { try { const post = findPost(req.params.id) if (!post) throw new AppError(404, 'Post not found') res.json(post) } catch (err) { next(err) }})Update the error handler to handle both AppError and unknown errors:
import { Request, Response, NextFunction } from 'express'import { AppError } from '../errors.js'
export function errorHandler( err: unknown, req: Request, res: Response, next: NextFunction) { if (err instanceof AppError) { return res.status(err.statusCode).json({ error: err.message }) }
console.error('Unexpected error:', err) res.status(500).json({ error: 'Internal server error' })}Handling async errors
Section titled “Handling async errors”Express 4 doesn’t automatically catch async errors. You must either:
Option 1: try/catch + next(err):
router.post('/posts', async (req, res, next) => { try { const post = await createPost(req.body) res.status(201).json(post) } catch (err) { next(err) }})Option 2: Async wrapper (cleaner):
import { Request, Response, NextFunction, RequestHandler } from 'express'
export function asyncHandler(fn: RequestHandler): RequestHandler { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next) }}router.post('/posts', asyncHandler(async (req, res) => { const post = await createPost(req.body) // errors caught automatically res.status(201).json(post)}))The Bulletin API uses better-sqlite3 which is synchronous, so async error handling is less critical — but asyncHandler is still good practice.
Exercise
Section titled “Exercise”- Create
src/errors.tswith theAppErrorclass. - Create
src/middleware/errorHandler.tswith the error handler. - Register it in
src/index.tsafter all routes. - Throw an
AppError(404, 'Post not found')from a route and verify you get the correct JSON response.
- Error middleware has 4 arguments:
(err, req, res, next)— Express identifies it by arity. - Must be registered last — after all routes and non-error middleware.
next(error)from any handler jumps to the error middleware.- A custom
AppErrorclass carries astatusCodefor clean, typed error responses. - Wrap async route handlers to automatically catch Promise rejections.