-
Notifications
You must be signed in to change notification settings - Fork 0
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.
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).
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}})));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'.
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.
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.
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).
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"
}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.