Skip to main content

extending

Composing multiple kits in one application, adding fields to a response at the HTTP layer without touching the kit, and what subclassing ErrorClass does and doesn't buy you.

Composing multiple kits

The recommended way to grow past a single registry is more kits, not a bigger one (see Core concepts). A top-level handler that needs to deal with several subsystems' kits checks each in turn:

import { AuthError } from './auth/errors'
import { BillingError } from './billing/errors'
import { StorageError } from './storage/errors'

function toResponse(err: unknown) {
  if (err instanceof AuthError) return { status: err.status, body: err.toJSON() }
  if (err instanceof BillingError) return { status: err.status, body: err.toJSON() }
  if (err instanceof StorageError) return { status: err.status, body: err.toJSON() }
  return { status: 500, body: { ok: false, error: { code: 'INTERNAL', status: 500 } } }
}

Because every kit's toJSON() returns the same { ok, error: { code, status, ...meta } } shape, this stays flat regardless of how many kits you add. There's no need for a common base class to get a common response shape.


Adding fields at the HTTP layer, not the kit

A common ask is attaching something request-scoped, such as a request id or a trace id, to every error response. Do this by decorating toJSON()'s output where you build the response, not by modifying the kit:

function respond(
  res: { status(code: number): { json(body: unknown): void } },
  err: KitError<any>,
  requestId: string,
) {
  const body = err.toJSON()
  res.status(err.status).json({ ...body, error: { ...body.error, requestId } })
}

This keeps the kit itself free of anything request-scoped. AppError has no idea an HTTP request exists, which is what makes it reusable from a queue worker, a CLI, or a test, none of which have a requestId to attach.


Subclassing ErrorClass

kit.ErrorClass is a real, ordinary JavaScript class. class AppError extends kit.ErrorClass { } works the same way extending any other class does, and an instance of the subclass still passes instanceof kit.ErrorClass and hasErrorCode.

class LoggedAppError extends AppError {
  constructor(...args: ConstructorParameters<typeof AppError>) {
    super(...args)
    logger.warn(this.code, this.meta)
  }
}