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
       │
       ▼
res.status(status).json(policy.errorBody(err))

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).

Unexpected post-response failures are forwarded to next(err) so the Express error pipeline can finish the socket — this covers both Express 4 (no async-handler auto-await) and Express 5 uniformly.

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:

createExpressAdapter(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('/planets', createExpressAdapter(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.codeerr.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 middleware:

app.use((req, res, next) => {
  if (!req.user) {
    return next(Object.assign(new Error('Authentication required'), {status: 401, code: 'Unauthorized'}));
  }
  next();
});

err.code sets the code field in the response body; err.message sets message. Errors reaching Express's default error handler are rendered generically — wrap the adapter (see below) if you need richer error bodies for errors raised outside the adapter.

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';

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

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

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

Note that policy.errorBody receives only err; the Express req 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 Express context

If you need request context in the error body (user ID, request ID, locale), install an Express error-handling middleware downstream — it catches anything the adapter forwards via next(err):

const inner = createExpressAdapter(adapter);
app.use('/planets', inner);

app.use((err, req, res, next) => {
  if (res.headersSent) return next(err);
  res.status(err.status || 500).json({
    code: err.code || err.name || 'Error',
    message: err.message,
    requestId: req.requestId,
    user: req.user?.id
  });
});

Note that the inner adapter already has its own try/catch — errors caught inside are already written via res.status().json(). The outer error handler only fires on truly unexpected throws (e.g. a rejection that escapes the adapter's dispatch, or errors in sibling 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 next(). The adapter never fabricates a 404 for unknown shapes; Express's default handler (or your own) gets the final say. This keeps the adapter composable.

Clone this wiki locally