Skip to content

Debugging & Diagnostics

ergo and ergo-router surface errors and warnings at three points in the request lifecycle: registration time (when routes are registered), factory time (when middleware is instantiated), and runtime (during request handling). This page documents each diagnostic mechanism and provides a reference for all warning codes.

Registration-Time Validation (ergo-router)

Section titled “Registration-Time Validation (ergo-router)”

When you register a route with router.get(), router.post(), etc., ergo-router validates the config object immediately — before any requests arrive.

Unknown config keys are caught with Levenshtein-distance suggestions:

router.post('/users', {
authorisation: {strategies: [bearerStrategy]},
execute: createUser,
});
Error: Unknown config key "authorisation" in route config for POST /users (did you mean "authorization"?)

ergo-router validates in strict mode by default — unknown keys throw an Error that prevents server startup. This catches typos before a single request is processed.

Type validation ensures every config key has the correct value type (object, boolean, function, or Array depending on the key):

router.post('/users', {
authorization: 'admin',
execute: createUser,
});
Error: Invalid "authorization" in route config for POST /users: expected object or boolean, got string.

Missing execute function — every declarative route config must include an execute function:

router.post('/users', {
authorization: {strategies: [bearerStrategy]},
validate: {body: userSchema},
});
Error: Missing "execute" function in route config for POST /users. Declarative route configs must include an execute function.

Semantic contradictions are detected across resolved config. For example, body: false combined with validate.body is caught at registration time:

router.post('/users', {
body: false,
validate: {body: userSchema},
execute: createUser,
});
Error: Route config for POST /users has body: false but validate.body is configured.
Body parsing must be enabled for body validation to work.

Each ergo middleware factory validates its options object when called. Unknown options produce a process.emitWarning() with a Levenshtein suggestion (distance ≤ 3):

(node:12345) [ERGO_AUTHORIZATION_UNKNOWN_OPTION] ErgoWarning:
authorization() received unknown option: "stratagies" (did you mean "strategies"?).
Valid options are: authorizer, challenge, realm, strategies.

Factory-time warnings use process.emitWarning() rather than throw so that a typo in an optional field does not prevent startup. The middleware still functions — only the unrecognized option is ignored.

ergo uses Node.js process.emitWarning() with {type: 'ErgoWarning', code} for runtime diagnostic messages. These warnings fire during request handling when the pipeline encounters a configuration issue that does not warrant a thrown error.

process.on('warning', (warning) => {
if (warning.type === 'ErgoWarning') {
console.error(`[${warning.code}] ${warning.message}`);
}
});

To see where a warning originates, start Node.js with the --trace-warnings flag:

Terminal window
node --trace-warnings server.js

This prints a full stack trace for each process.emitWarning() call, making it straightforward to trace the warning back to the specific middleware factory or pipeline step.

CodeSourceConditionAction
ERGO_VALIDATE_NO_BODYvalidate()validate.body schema is configured but acc.body is missing — body() middleware did not run before validate()Ensure body is in the pipeline before validate, or let ergo-router auto-include it
ERGO_VALIDATE_UNKNOWN_KEYvalidate()Schema object contains keys other than body, query, paramsCheck for typos; valid keys are body, query, params

Each middleware factory generates a warning code using the pattern ERGO_{NAME}_UNKNOWN_OPTION where {NAME} is the uppercase factory name with camelCase boundaries expanded:

Code PatternFactory
ERGO_ACCEPTS_UNKNOWN_OPTIONaccepts()
ERGO_AUTHORIZATION_UNKNOWN_OPTIONauthorization()
ERGO_BODY_UNKNOWN_OPTIONbody()
ERGO_CACHE_CONTROL_UNKNOWN_OPTIONcacheControl()
ERGO_COMPRESS_UNKNOWN_OPTIONcompress()
ERGO_COOKIE_UNKNOWN_OPTIONcookie()
ERGO_CORS_UNKNOWN_OPTIONcors()
ERGO_CSRF_UNKNOWN_OPTIONcsrf()
ERGO_HANDLER_UNKNOWN_OPTIONhandler()
ERGO_IDEMPOTENCY_UNKNOWN_OPTIONidempotency()
ERGO_LOGGER_UNKNOWN_OPTIONlogger()
ERGO_PAGINATE_UNKNOWN_OPTIONpaginate()
ERGO_PRECONDITION_UNKNOWN_OPTIONprecondition()
ERGO_RATE_LIMIT_UNKNOWN_OPTIONrateLimit()
ERGO_SECURITY_HEADERS_UNKNOWN_OPTIONsecurityHeaders()
ERGO_SEND_UNKNOWN_OPTIONsend()
ERGO_TIMEOUT_UNKNOWN_OPTIONtimeout()
ERGO_TRACING_UNKNOWN_OPTIONtracing()

All dynamic codes include the unrecognized option name(s) and a “did you mean?” suggestion when a valid option is within Levenshtein distance ≤ 3.

ergo-router does not use process.emitWarning(). Its config validation uses:

  • Strict mode (default): throw new Error() for unknown config keys at registration time
  • Lenient mode (strict: false): console.warn() for unknown config keys — prefixed with [ergo-router]

In lenient mode, unknown keys produce a warning instead of throwing:

const router = createRouter({strict: false});
router.get('/users', {
rateLimmit: {max: 100},
execute: listUsers,
});
[ergo-router] Unknown config key "rateLimmit" in route config for GET /users (did you mean "rateLimit"?)

The server starts normally and the unrecognized key is ignored. All other validations (type checks, missing execute, semantic contradictions) still throw regardless of the strict setting.

For request-level debugging (identifying which middleware rejected a request), enable pipeline debug mode with debug: true. This populates _trace in error response bodies, showing the execution path and break point.

See the Debug Tracing recipe for full usage examples and the _trace shape reference.

The onResponse lifecycle hook fires after send() completes. Errors thrown by the hook are silently swallowed — they do not appear in the HTTP response or cause unhandled rejections. If your onResponse hook appears to not fire:

  1. Check application logs — hook errors are caught internally
  2. Verify that no catchHandler is configured on the route — onResponse does not fire when a catchHandler handles the error via early return

See Lifecycle Hooks for the execution model and handler onResponse for the standalone equivalent.

Unlike registration-time errors (which prevent startup), runtime errors are returned as HTTP responses using RFC 9457 Problem Details format (Content-Type: application/problem+json). Every error response includes type, title, status, and detail.

401 Unauthorized — missing or invalid Bearer token:

{
"type": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401",
"title": "Unauthorized",
"status": 401,
"detail": "Unauthorized"
}

422 Unprocessable Entity — JSON Schema validation failure with per-field details:

{
"details": [
{
"path": "/name",
"message": "must NOT have fewer than 1 characters",
"params": {"limit": 1}
}
],
"type": "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422",
"title": "Unprocessable Entity",
"status": 422,
"detail": "Validation failed"
}

The details extension member is an array of per-field errors produced by the validate middleware. Each entry includes the JSON Pointer path, the AJV error message, and keyword-specific params.

For the complete catalog of every error status code, condition, and producing middleware, see the Error Response Reference.

Symptom: Server fails to start with an error like:

Error: Route config for POST /users has body: false but validate.body is configured.

Cause: The route (or inherited defaults) sets body: false while also configuring validate.body. Body parsing must run before body validation.

Fix: Remove body: false from the route config, or remove the validate.body schema if body validation is not needed.

Symptom: PUT or PATCH requests return 428 Precondition Required even though the request body is correct.

Cause: The route has preconditionRequired: true (or inherits it from defaults), which enforces that clients send an If-Match or If-Unmodified-Since header. This is independent of ETag generation — ETags enable conditional evaluation, while preconditionRequired enforces header presence.

Fix: Send an If-Match header with the resource’s current ETag, or set preconditionRequired: false on the route if enforcement is not needed. See the precondition middleware guide for the full interaction.

Symptom: A warning appears in the console:

(node:12345) [ERGO_AUTHORIZATION_UNKNOWN_OPTION] ErgoWarning:
authorization() received unknown option: "stratagies" (did you mean "strategies"?).

Cause: A middleware option name is misspelled. The middleware still functions but ignores the unrecognized option.

Fix: Correct the option name as suggested. Run with --trace-warnings to see the call site.

Symptom: Responses are not compressed even though compress is configured.

Cause: The Accept-Encoding request header does not include a supported encoding (gzip, deflate, br), or the response body is below the minimum size threshold.

Fix: Verify the request includes Accept-Encoding: gzip (or another supported encoding). Check the compress middleware’s threshold option — bodies smaller than the threshold are not compressed.

Symptom: The onResponse callback never executes.

Cause: A catchHandler is configured on the route or router. When the pipeline throws, the catchHandler takes control via early return — onResponse hooks do not fire in this path.

Fix: If you need post-response observation alongside error handling, add logging inside the catchHandler itself. See Lifecycle Hooks for the execution model.