Skip to main content

Express

A terminal error-handling middleware that maps @gentleduck/error instances to JSON responses, with a fallback for everything else.

Overview

Express identifies error-handling middleware by arity: a handler with four parameters (err, req, res, next), and dispatches to it whenever a route calls next(err) or throws inside an async handler wrapped to forward its rejection. Register one at the end of the middleware chain, after every route.

Full runnable example: examples/express.

Error middleware

import type { ErrorRequestHandler } from 'express'
import { AppError } from './errors'

export const errorHandler: ErrorRequestHandler = (err, _req, res, _next) => {
  if (err instanceof AppError) {
    res.status(err.status).json(err.toJSON())
    return
  }
  // eslint-disable-next-line no-console
  console.error(err)
  res.status(500).json({ ok: false, error: { code: 'INTERNAL', status: 500 } })
}
import express from 'express'
import { errorHandler } from './error-handler'
import { widgetsRouter } from './widgets'

const app = express()
app.use('/widgets', widgetsRouter)

// Registered last: Express only treats a 4-arg handler as error middleware,
// and only routes reached after this point in the chain forward to it.
app.use(errorHandler)

app.listen(3000)

Throwing from a route

Express does not catch a rejected promise from an async handler on its own. Forward it to next explicitly, or wrap the handler:

import { Router } from 'express'
import { throwAppError } from './errors'

export const widgetsRouter = Router()

widgetsRouter.get('/:id', async (req, res, next) => {
  try {
    const widget = await db.widgets.findById(req.params.id)
    if (!widget) throwAppError('NOT_FOUND', { resource: 'widget', id: req.params.id })
    res.json(widget)
  } catch (err) {
    next(err)
  }
})