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.

Code Source Timing Condition Action
ERGO_CORS_WILDCARD_CREDENTIALS cors() factory origins: '*' with allowCredentials: true — OWASP-documented misconfiguration allowing any site to make credentialed requests Replace the wildcard with an explicit origin allowlist; see the cors middleware guide
ERGO_IDEMPOTENCY_PROCESSING_EVICTED IdempotencyStore runtime All entries are processing (no completed entries to evict) and a new entry must be stored — oldest processing entry is evicted Increase maxKeys to match expected concurrent request volume, or investigate why requests are not calling complete()
ERGO_VALIDATE_NO_BODY validate() runtime 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_KEY validate() factory Schema object contains keys other than body, query, params Check 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 Pattern Factory
ERGO_ACCEPTS_UNKNOWN_OPTION accepts()
ERGO_AUTHORIZATION_UNKNOWN_OPTION authorization()
ERGO_BODY_UNKNOWN_OPTION body()
ERGO_CACHE_CONTROL_UNKNOWN_OPTION cacheControl()
ERGO_COMPRESS_UNKNOWN_OPTION compress()
ERGO_COOKIE_UNKNOWN_OPTION cookie()
ERGO_CORS_UNKNOWN_OPTION cors()
ERGO_CSRF_UNKNOWN_OPTION csrf()
ERGO_HANDLER_UNKNOWN_OPTION handler()
ERGO_IDEMPOTENCY_UNKNOWN_OPTION idempotency()
ERGO_LOGGER_UNKNOWN_OPTION logger()
ERGO_PAGINATE_UNKNOWN_OPTION paginate()
ERGO_PRECONDITION_UNKNOWN_OPTION precondition()
ERGO_RATE_LIMIT_UNKNOWN_OPTION rateLimit()
ERGO_SECURITY_HEADERS_UNKNOWN_OPTION securityHeaders()
ERGO_SEND_UNKNOWN_OPTION send()
ERGO_TIMEOUT_UNKNOWN_OPTION timeout()
ERGO_TRACING_UNKNOWN_OPTION tracing()
ERGO_VALIDATE_UNKNOWN_OPTION validate()

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
  3. For router-level hooks, check whether the response is transport-level (404, 405, 415, 429, OPTIONS, CORS preflight) — domainAcc is undefined for these responses. Use responseInfo.source to distinguish pipeline vs transport responses and optional chaining (domainAcc?.auth?.identity) to safely access pipeline-only data

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. Additionally, per-route onResponse hooks only fire for pipeline responses — they do not fire for transport-level short-circuits (404, 405, 415, 429, OPTIONS, CORS preflight). Only the router-level onResponse hook fires for transport responses.

Fix: If you need post-response observation alongside error handling, add logging inside the catchHandler itself. If you need to observe transport-level responses, use a router-level onResponse hook and check responseInfo.source to distinguish pipeline vs transport responses. See Lifecycle Hooks for the execution model.