Error Handling
Robust error handling is essential for creating reliable and user-friendly APIs. Jetpath encourages centralizing error handling logic within your middleware, providing a consistent way to catch errors, log them, and send standardized responses to the client.
Philosophy
Instead of scattering try...catch
blocks throughout your route handlers for common errors, Jetpath’s approach relies on:
- Signaling Errors: Using
ctx.throw()
or standardthrow new Error()
within route handlers or preceding middleware logic when something goes wrong. - Centralized Catching: Intercepting these thrown errors within the post-handler function returned by your middleware.
- Standardized Responses: Formatting and sending consistent error responses from the middleware’s error handling block.
Throwing Errors
When you encounter a situation in your route handler (or pre-handler middleware logic) where processing cannot continue normally, you should signal an error. The preferred way is using ctx.throw()
.
Using ctx.throw()
The ctx.throw(codeOrData, message?)
method is designed specifically for signaling HTTP-related errors. It sets the ctx.code
(status code) and attaches an error message, then throws an error that interrupts the current execution flow.
-
Common Status Codes:
400 Bad Request
: Validation errors, malformed requests.401 Unauthorized
: Missing or invalid authentication credentials.403 Forbidden
: Authenticated user lacks permission for the action.404 Not Found
: Resource doesn’t exist.500 Internal Server Error
: Unexpected server-side issues.
-
Examples:
// 1. Resource Not Found if (!pet) { ctx.throw(404, `Pet with ID ${ctx.params.id} not found`); }
// 2. Unauthorized Access const authResult = ctx.plugins.verifyAuth(ctx); if (!authResult.authenticated) { ctx.set("WWW-Authenticate", "Bearer realm="protected""); // Set relevant header ctx.throw(401, authResult.message || "Authentication required"); }
// 3. Forbidden Action if (ctx.app.user?.role !== 'admin') { ctx.throw(403, "Admin privileges required"); }
// 4. Validation Error (Bad Request) try { const validatedData = ctx.validate(RequestSchema); } catch (validationError) { // Pass validation message, set code to 400 ctx.throw(400, validationError.message); }
// 5. Simple Not Found (message optional) ctx.throw(404);
[cite: examples inspired by logic in tests/app.jet.ts]
Using throw new Error()
You can also use standard JavaScript throw new Error("Something broke")
. This is suitable for unexpected internal errors. When caught by the middleware, ctx.code
might not be set, so your middleware should typically default to a 500 Internal Server Error
status code in these cases.
try {
const result = await riskyOperation();
} catch (internalError) {
console.error("Internal operation failed:", internalError);
// Let the middleware handle formatting, just throw
throw new Error("Internal processing error occurred.");
}
Handling Errors in Middleware
The post-handler function returned by your MIDDLEWARE_
definition is the primary location for catching and handling errors thrown from route handlers, validation logic (ctx.validate
), or the pre-handler part of the middleware itself.
The err
Parameter
This function receives the ctx
object and an optional second argument, typically named err
.
- If the route handler and pre-handler logic completed without throwing,
err
will beundefined
. - If any error was thrown (using
ctx.throw()
orthrow new Error()
),err
will be theError
object that was thrown.
Middleware Error Handling Block
Here’s how to structure the error handling within your middleware’s returned function, based on the example in tests/app.jet.ts
:
// Inside the function returned by MIDDLEWARE_
return (ctx, err?: Error) => {
// =========================
// === POST-HANDLER CODE ===
// =========================
const duration = Date.now() - startTime; // Assuming startTime from pre-handler
ctx.set("X-Response-Time", `${duration}ms`);
// — Central Error Handling Logic —
if (err) {
// 1. Log the error (essential for debugging)
// Use a proper logger plugin in production
console.error({
message: Request Error: <span class="hljs-subst">${err.message}</span>
,
stack: err.stack,
requestId: ctx.get("X-Request-ID"), // Include request context
url: ctx.request.url,
method: ctx.request.method,
});
// Example with logger plugin:
// ctx.plugins.logger?.error({ error: err.message, stack: err.stack, code: ctx.code, requestId: ctx.get("X-Request-ID") });
<span class="hljs-comment">// 2. Determine the Status Code</span>
<span class="hljs-comment">// Use ctx.code if it was set by ctx.throw() or before throwing.</span>
<span class="hljs-comment">// Default to 500 for unexpected errors (where ctx.code might still be 200 or unset).</span>
ctx.<span class="hljs-property">code</span> = ctx.<span class="hljs-property">code</span> >= <span class="hljs-number">400</span> ? ctx.<span class="hljs-property">code</span> : <span class="hljs-number">500</span>;
<span class="hljs-comment">// 3. Format the Error Response Body</span>
<span class="hljs-comment">// Avoid leaking sensitive stack traces in production for 5xx errors!</span>
<span class="hljs-keyword">const</span> errorMessage = (ctx.<span class="hljs-property">code</span> >= <span class="hljs-number">500</span> && process.<span class="hljs-property">env</span>.<span class="hljs-property">NODE_ENV</span> === <span class="hljs-string">'production'</span>)
? <span class="hljs-string">"Internal Server Error"</span>
: err.<span class="hljs-property">message</span> || <span class="hljs-string">"An unexpected error occurred"</span>;
<span class="hljs-keyword">const</span> errorResponse = {
<span class="hljs-attr">error</span>: {
<span class="hljs-attr">message</span>: errorMessage,
<span class="hljs-attr">code</span>: ctx.<span class="hljs-property">code</span>,
<span class="hljs-attr">requestId</span>: ctx.<span class="hljs-title function_">get</span>(<span class="hljs-string">"X-Request-ID"</span>), <span class="hljs-comment">// Helpful for tracing</span>
<span class="hljs-comment">// Optionally include validation details for 400 errors if safe</span>
<span class="hljs-comment">// details: (ctx.code === 400 && err.details) ? err.details : undefined,</span>
},
<span class="hljs-attr">timestamp</span>: <span class="hljs-keyword">new</span> <span class="hljs-title class_">Date</span>().<span class="hljs-title function_">toISOString</span>(),
};
<span class="hljs-comment">// 4. Send the Error Response</span>
<span class="hljs-comment">// Ensure correct Content-Type (usually application/json)</span>
ctx.<span class="hljs-title function_">set</span>(<span class="hljs-string">"Content-Type"</span>, <span class="hljs-string">"application/json"</span>);
ctx.<span class="hljs-title function_">send</span>(errorResponse);
<span class="hljs-comment">// 5. IMPORTANT: Stop further processing</span>
<span class="hljs-comment">// Prevent subsequent logic (like 404 checks) from running</span>
<span class="hljs-keyword">return</span>;
} // — End of Error Handling —
// … Handle successful responses or 404s if no error occurred …
// (As shown in the Middleware documentation page)
};
[cite: Based on error handling logic in MIDDLEWARE_
in tests/app.jet.ts]
Specific Error Examples
- 404 Not Found: If
err
isundefined
(no error thrown) and the route handler didn’t send a response, your post-handler middleware logic should detect this (e.g., by checkingctx.response.status === 404
or ifctx.response.body
is null/undefined) and send a standard 404 response. - 400 Bad Request (Validation): When
ctx.validate()
fails, it throws an error. Your middleware catches this.ctx.throw(400, validationError.message)
is a good way to trigger this from the handler, ensuring the middleware sets code 400 and uses the validation message. - 401 Unauthorized / 403 Forbidden: Throw these using
ctx.throw(401, "...")
orctx.throw(403, "...")
. The middleware catches them and sends the response using the specified code and message. Consider adding appropriate headers likeWWW-Authenticate
viactx.set()
before throwing 401. - 500 Internal Server Error: Catch unexpected errors (e.g., from
throw new Error()
or database issues). Setctx.code = 500
and send a generic error message in production to avoid leaking implementation details. Log the full error server-side.
Best Practices
- Centralize: Implement your primary error handling in global middleware for consistency.
- Standardize Responses: Use a consistent JSON structure for error responses (e.g.,
{ error: { message: string, code: number, requestId?: string } }
or follow RFC 7807 Problem Details). - Log Effectively: Log errors with sufficient context (request ID, URL, user ID if available, full stack trace) on the server.
- Don’t Leak Sensitive Information: Avoid sending stack traces or detailed internal error messages to the client, especially for 5xx errors in production environments.
- Use
ctx.throw()
: Preferctx.throw()
for signaling HTTP-specific errors with appropriate status codes.
Next Steps
- Review the Middleware documentation for the full middleware structure.
- See how the Context (
ctx
) Object methods (throw
,send
,code
,set
) are used.