Skip to content
This repository was archived by the owner on Jul 17, 2026. It is now read-only.

Error handling

Eugene Lazutkin edited this page Apr 19, 2026 · 1 revision

Error handling

The adapter wraps every route handler in a try / catch. Thrown errors are mapped to a status code and serialized into a JSON body. Callers never see stack traces or raw SDK errors unless you opt in.

Flow

adapter throws / parser rejects
       │
       ▼
is err.status an HTTP status (400–599)?
       │
   yes ┤──► use err.status directly
       │
       ▼ no
mapErrorStatus(err, policy.statusCodes) → status
       │
       ▼
ctx.body = policy.errorBody(err)
ctx.status = status

Sources of thrown errors:

  • The Adapter (CRUD methods, hooks, expression builders, batch retries).
  • The request body / query parsers in rest-core (validation rejections).
  • The adapter itself (unknown method on a known route → 405, bad JSON → 400, oversized body → 413).

Default status mapping

mapErrorStatus routes common SDK error names to buckets defined in policy.statusCodes:

SDK error name Default status Policy bucket
ConditionalCheckFailedException 409 consistency
TransactionCanceledException 409 consistency
TransactionConflictException 409 consistency
ValidationException / ValidationError 422 validation
ProvisionedThroughputExceededException 429 throttle
RequestLimitExceeded 429 throttle
ItemCollectionSizeLimitExceededException 429 throttle
LimitExceededException 429 throttle
InternalServerError / ServiceUnavailable 503 transient
Other 5xx HTTP from SDK 503 transient
anything else 500 internal

Override any bucket via policy.statusCodes:

createKoaAdapter(adapter, {
  policy: {
    statusCodes: {
      miss:        404,
      validation:  400,  // 400 instead of 422
      consistency: 409,
      throttle:    503,  // coalesce throttle + transient
      transient:   503,
      internal:    500
    }
  }
});

policy.statusCodes requires all keys because the upstream RestStatusCodes type is non-partial. Supply the full map or start from defaultPolicy.statusCodes:

import {defaultPolicy} from 'dynamodb-toolkit/rest-core';

const statusCodes = {...defaultPolicy.statusCodes, validation: 400};
app.use(mount('/planets', createKoaAdapter(planets, {policy: {statusCodes}})));

Default error body

policy.errorBody defaults to buildErrorBody from rest-core:

{
  "code":    "ConditionalCheckFailedException",
  "message": "The conditional request failed"
}

code falls back to err.code → err.name → 'Error'. message falls back to 'Unknown error'.

Throwing your own errors

Any error object with a status property in the 400–599 range passes through to the response as-is. Useful for auth / validation rejections in your own hooks or wrappers:

app.use(async (ctx, next) => {
  if (!ctx.state.user) {
    throw Object.assign(new Error('Authentication required'), {status: 401, code: 'Unauthorized'});
  }
  await next();
});

err.code sets the code field in the response body; err.message sets message.

Including debug info in development

buildErrorBody accepts an includeDebug flag that attaches err.stack. Wire it via a custom policy.errorBody:

import {buildErrorBody} from 'dynamodb-toolkit/rest-core';

const isDev = process.env.NODE_ENV !== 'production';

createKoaAdapter(adapter, {
  policy: {
    errorBody: err => buildErrorBody(err, {includeDebug: isDev})
  }
});

Also supports per-request correlation IDs via the errorId option:

policy: {
  errorBody: (err, /* ctx unavailable here */) =>
    buildErrorBody(err, {errorId: crypto.randomUUID()})
}

Note that policy.errorBody receives only err; the Koa ctx isn't threaded through. For per-request IDs it's usually easier to set a response header in upstream middleware and leave the body shape default.

Custom error body with Koa context

If you need request context in the error body (user ID, request ID, locale), wrap the middleware instead of overriding policy.errorBody:

const inner = createKoaAdapter(adapter);

app.use(async (ctx, next) => {
  try {
    await inner(ctx, next);
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = {
      code: err.code || err.name || 'Error',
      message: err.message,
      requestId: ctx.state.requestId,
      user: ctx.state.user?.id
    };
  }
});

Note that the inner adapter already has its own try/catch — errors caught inside are already written to ctx.body. The outer wrapper only fires on truly unexpected throws (e.g. next() rejection from downstream middleware).

405 on wrong method

When the route shape is recognized but the method isn't supported (e.g. POST /:key), the adapter responds with:

HTTP/1.1 405 Method Not Allowed
{
  "code": "MethodNotAllowed",
  "message": "Method not allowed for this route"
}

Unrecognized routes

Paths that don't match any shape in matchRoute (e.g. three-segment paths) pass through to the next middleware via await next(). The adapter never fabricates a 404 for unknown shapes; Koa's default handler (or your own) gets the final say. This keeps the adapter composable.

Clone this wiki locally