hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Logging and Observability with @hectoday/http

Why Logging

  • console.log Is Not Enough
  • What to Log
  • Project Setup

Structured Logging

  • JSON Logs
  • Log Levels
  • Building a Logger

Request Logging

  • Request IDs
  • Request-Response Logging
  • Error Logging

Context and Correlation

  • Log Context
  • Child Loggers
  • Correlating Across Services

What to Observe

  • Business Event Logging
  • Performance Logging
  • Health and Metrics

Production

  • Log Output and Transport
  • Sensitive Data
  • Checklist and Capstone

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 onError callback 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?

← Request-Response Logging Log Context →

© 2026 hectoday. All rights reserved.