Course contentsShow
← Node.js
Lesson 1399 of 1,98013. Error Handling and Resilient SystemsFree lesson

Differentiating Expected Errors and Exceptions

You will categorize code failures into predicted business logic violations versus unforeseen runtime bugs.

In this lesson, you will learn how to distinguish between anticipated business logic violations and unexpected runtime failures to build more resilient Node.js applications.

As we move away from complex caching architectures, we must confront the reality of failure. In Node.js, not all errors are created equal. We categorize them into two distinct buckets: Expected Errors and Exceptions.

Expected Errors (Operational)

These are failures you can predict because they represent valid, albeit unfortunate, business scenarios. For example, if a user provides an incorrect password or a requested resource is missing (404), your application should handle these gracefully. They aren't "bugs"; they are part of your system's normal flow. You should handle these using explicit logic, such as returning a specific error object or a custom response.

async function getUser(id) {
  const user = await db.find(id);
  if (!user) {
    // Expected: The user doesn't exist, handle as a business flow
    return { error: 'USER_NOT_FOUND', status: 404 };
  }
  return user;
}

Exceptions (Programmer Errors)

Exceptions represent unforeseen states or bugs that indicate your code is in an invalid condition—like trying to read a property of undefined, a syntax error, or a failed connection to a critical dependency. These are truly "exceptional." You cannot handle these with standard logic flows because the state of your application is compromised.

// This is an Exception
const data = JSON.parse(userInput); 
// If userInput is malformed JSON, this throws an error.
// You didn't expect the code to crash here; it's a runtime bug.

The Strategy

The core philosophy is to design for the expected and guard against the exceptional.

  1. For Expected Errors: Use explicit returns or functional patterns (like Result types) to pass information back to the caller. Do not use try/catch for flow control.
  2. For Exceptions: Let them bubble up or handle them at the process level (via global uncaughtException handlers) to log the diagnostic data, clean up resources, and perform a clean restart if necessary.

By separating these, you stop polluting your logic with catch blocks meant for things that shouldn't happen, and you gain a clear signal on what requires an immediate code fix versus what requires a user-friendly error message.

Key Takeaway: Distinguish between expected business states that require graceful feedback and genuine exceptions that represent broken assumptions in your code.