Error Logging
Errors need the most context
When an error occurs, you need everything: what happened, where, why, who was affected, and what the system was doing at the time. The request-response log tells you the status code (500) and duration. The error log tells you the cause.
Logging in onError
Hectoday HTTP’s onError callback catches all unhandled errors:
onError: ({ error, request, locals }) => {
const url = new URL(request.url);
logger.error("unhandled error", {
requestId: locals?.requestId,
method: request.method,
path: url.pathname,
error: error.message,
stack: error.stack,
name: error.name,
});
return Response.json(
{ error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred" } },
{ status: 500 }
);
}, The log entry includes everything needed to debug: the request ID (to correlate with other logs), the request details, the error message, and the stack trace. The response to the client is generic — no error details leak.
[!NOTE] The Error Handling course’s
onErrorcallback returned formatted error responses. Now it also logs the error with full context before returning the response.
Logging operational vs programmer errors
The Error Handling course distinguished operational errors (expected: validation, not found) from programmer errors (unexpected: TypeError, null reference). Log them differently:
onError: ({ error, request, locals }) => {
const url = new URL(request.url);
const context = {
requestId: locals?.requestId,
method: request.method,
path: url.pathname,
};
if (error instanceof AppError) {
// Operational error — expected, handled
logger.warn("operational error", {
...context,
code: error.code,
message: error.message,
statusCode: error.statusCode,
});
return Response.json(
{ error: { code: error.code, message: error.message } },
{ status: error.statusCode }
);
}
// Programmer error — unexpected, needs investigation
logger.error("programmer error", {
...context,
error: error.message,
stack: error.stack,
name: error.name,
});
return Response.json(
{ error: { code: "INTERNAL_ERROR", message: "An unexpected error occurred" } },
{ status: 500 }
);
}, Operational errors (validation failure, not found) are logged at warn — they are expected. Programmer errors (TypeError, undefined access) are logged at error with a stack trace — they are bugs.
Stack traces in JSON
Stack traces are multi-line strings. In JSON, they become a single escaped string:
{
"level": "error",
"message": "programmer error",
"error": "Cannot read properties of undefined",
"stack": "TypeError: Cannot read properties of undefined (reading 'id')\n at getBook (src/routes/books.ts:42:18)\n at resolve (src/routes/books.ts:15:20)"
} The \n newlines are preserved in the JSON string. Log tools display them correctly when you view the entry.
Error log enrichment
Add more context when available:
// In a route handler, before calling a service
try {
const result = await chargeCard(order.total, paymentToken);
} catch (err) {
logger.error("payment failed", {
requestId: c.locals.requestId,
userId: c.locals.userId,
orderId: order.id,
amount: order.total,
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
});
throw new AppError("PAYMENT_FAILED", "Payment processing failed", 502);
} The log includes the order ID, user ID, and amount — context that the generic onError handler would not have. The AppError is then caught by onError and logged at warn level.
Exercises
Exercise 1: Add error logging to onError. Trigger an error by throwing in a route handler. Verify the log includes the stack trace.
Exercise 2: Distinguish operational and programmer errors. Log operational at warn, programmer at error. Trigger both.
Exercise 3: Enrich an error log with business context (user ID, order ID). Verify the extra fields appear in the log.
Why log the stack trace for programmer errors but not for operational errors?