-
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
│
▼
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.
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}}));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 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.
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.
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).
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 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.