hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Error Handling and Resilience with @hectoday/http

The problem with errors

  • Why error handling matters
  • Project setup

Error fundamentals

  • JavaScript error types
  • Try-catch and error propagation
  • Async errors

Structured error handling

  • Custom error classes
  • A global error handler
  • Operational vs programmer errors

Resilience patterns

  • Retries
  • Timeouts
  • Circuit breakers
  • Fallbacks and degradation

Server lifecycle

  • Graceful shutdown
  • Uncaught exceptions and unhandled rejections
  • Health checks under failure

Putting it all together

  • Error handling checklist
  • Capstone: resilient e-commerce API

Operational vs programmer errors

Two kinds of errors

Our global error handler treats AppError and non-AppError differently. But why? What is fundamentally different about a “Product not found” error and a “Cannot read properties of undefined” error?

The answer is that they are completely different kinds of problems, and they need completely different responses.

Operational errors are expected failures that happen during normal operation. The user sends invalid data. A database record does not exist. The payment service times out. These are not bugs. They are things that happen regularly, and the app should handle them gracefully.

Programmer errors are bugs in the code. Accessing a property on undefined. Calling a function with the wrong number of arguments. Using the wrong variable name. These should not happen. They indicate code that needs to be fixed.

The difference matters

Here is why this distinction changes everything about how you respond:

OperationalProgrammer
Example”Product not found""Cannot read properties of undefined”
Expected?YesNo
User’s fault?OftenNever
ResponseSpecific error (404, 400)Generic 500
ActionReturn error and continueLog, alert, and investigate
Recoverable?YesNot without a code fix

When a user requests a product that does not exist, that is operational. You return a 404, the user sees “Product not found,” and the server keeps running. Normal day.

When your code tries to read user.name on undefined, that is a programmer error. Something is wrong with the code. The user should see a generic “An unexpected error occurred” (because the real message leaks internal details), and the developer needs to investigate.

Return vs throw

This distinction maps perfectly to how we handle errors in our code.

Operational errors are returned. When the route handler knows what went wrong, it returns an error response directly. No throwing, no propagation. The route has all the context it needs to give the user a useful response:

route.get("/products/:id", {
  request: { params: z.object({ id: z.string() }) },
  resolve: (c) => {
    if (!c.input.ok)
      return validationFailed(
        c.input.issues.map((i) => ({ field: i.path.join("."), message: i.message })),
      );

    const product = db.prepare("SELECT * FROM products WHERE id = ?").get(c.input.params.id);
    if (!product) return notFound("Product"); // Operational: return directly

    return Response.json(product);
  },
});

Programmer errors are thrown (unintentionally). They happen because something unexpected went wrong. Nobody wrote a throw statement. A TypeError fired because a variable was undefined when it should not have been. These bubble up to the global error handler:

route.get("/products/:id", {
  request: { params: z.object({ id: z.string() }) },
  resolve: (c) => {
    if (!c.input.ok)
      return validationFailed(
        c.input.issues.map((i) => ({ field: i.path.join("."), message: i.message })),
      );

    const product = db.prepare("SELECT * FROM products WHERE id = ?").get(c.input.params.id) as any;
    return Response.json({ name: product.name }); // Bug: product might be undefined
    // TypeError thrown, caught by onError, returns generic 500
  },
});

The global handler catches it, logs the full stack trace for the developer, and returns a generic 500 to the client.

Preventing programmer errors

Here is a pattern you will use constantly. Sometimes a programmer error is entirely predictable, and you can prevent it by checking first:

function getProduct(id: string): Product | undefined {
  return db.prepare("SELECT * FROM products WHERE id = ?").get(id) as Product | undefined;
}

// In the route:
const product = getProduct(c.input.params.id);
if (!product) return notFound("Product"); // Check before accessing properties

return Response.json({ name: product.name }); // Safe: product is definitely defined

Without the if (!product) check, the caller would try to access properties on undefined and get a TypeError. That is a programmer error. It triggers a generic 500, and the user sees “An unexpected error occurred.”

With the check, we return a notFound response instead. That is an operational error. The user sees “Product not found.” Same situation, much better outcome.

This is the pattern every course in this series uses. Check for null/undefined before accessing properties, and return a specific error response when the check fails.

When to crash

Most errors should be handled without crashing. But some errors are so severe that continuing is dangerous:

// These should crash the process:
// - Out of memory
// - Failed to bind to port (another process is using it)
// - Database file is corrupted
// - Required environment variable is missing at startup

// Example: startup validation
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
  console.error("FATAL: JWT_SECRET is not set");
  process.exit(1); // Cannot run without this, crash immediately
}

The rule is: if the error means the app cannot function correctly, crash and let the container orchestrator restart it (as covered in the Deploying with Docker course). If the error is specific to one request, handle it and keep serving other requests.

Missing a JWT secret at startup? Crash. One user requesting a product that does not exist? Return a 404 and move on.

Exercises

Exercise 1: Intentionally cause a TypeError in a route handler (access a property on undefined). Verify the global error handler returns a generic 500 and logs the stack trace.

Exercise 2: Add a null check before the same property access and return notFound() instead. Verify the response is now a clean 404 with a specific message.

Exercise 3: Add a startup check for a required environment variable. Start the server without it. Verify it exits immediately with a clear error message.

We now have a complete structured error handling system: response helpers for expected errors, error classes for service-level errors, a global handler as a safety net, and a clear distinction between operational and programmer errors. But handling errors is only half the battle. What about preventing them in the first place? In the next section, we will build resilience patterns that help our app survive failures: retries, timeouts, circuit breakers, and fallbacks.

Why should programmer errors not be sent to the client?

← A global error handler Retries →

© 2026 hectoday. All rights reserved.