DEV Community

Gavin Hemsada
Gavin Hemsada

Posted on

Senior Devs Don’t Just Catch Errors—They Manage Them

When you’re a junior developer, a try-catch block feels like a safety net. If you’re worried a piece of code might crash, you wrap it in a try-catch and move on. But as you scale to 50,000+ users, you quickly realize that swallowing errors is often more dangerous than letting the app crash.

A blind try-catch hides the root cause of failures, makes debugging a nightmare, and can leave your application in an inconsistent state (e.g., money deducted from a user’s balance, but the order never created because the error was caught and ignored).

Senior engineers don't just "handle" exceptions; they design Error Policies. Here are the 4 patterns that separate the pros from the amateurs.

Pattern 1: The Global Error Middleware (Centralized Handling)

In a massive Express.js app, you shouldn't have unique error-handling logic in every single route. If you have 100 endpoints, that’s 100 places where a developer might forget to log an error or send the wrong HTTP status code.

The Senior Approach: Use a centralized error-handling middleware. Your routes should only be responsible for catching the error and passing it to the next() function. This ensures that every error in your system is formatted, logged, and reported to your monitoring tools (like Sentry or Datadog) in exactly the same way.

Pattern 2: The Circuit Breaker Pattern

If your Node.js API relies on a third-party service (like a Stripe or a weather API) and that service goes down, your app shouldn't keep trying to hit it. If you have 5,000 users all waiting for a 30-second timeout from a dead external service, your Node.js event loop will grind to a halt.

The Senior Approach: Implement a Circuit Breaker. If the external service fails 5 times in a row, the circuit trips. For the next 30 seconds, your app doesn't even try to hit the external API; it immediately returns a cached result or a Service Temporarily Unavailable message. This prevents a "cascading failure" from taking down your entire backend.

Pattern 3: Functional Error Returns (The Result Pattern)
Sometimes, an error isn't an exception—it's an expected outcome. For example, a user entering the wrong password isn't a "system crash"; it's a valid business logic branch. Using throw for expected logic is expensive and messy.

The Senior Approach: Return a Result object. Instead of throwing an error, return an object like { success: false, error: 'INVALID_CREDENTIALS' }. This forces the calling function to explicitly handle both the success and failure cases without the overhead of a stack trace.

Pattern 4: Operational vs. Programmer Errors

This is the biggest distinction.

Programmer Errors (Bugs): A variable is undefined, or you tried to read a property of null. These should crash the process. You want the server to restart so it doesn't stay in a broken state.

Operational Errors (Run-time): The database is busy, or a file wasn't found. These should be handled gracefully.

The Senior Approach: Distinguish between the two. Use a tool like process.on('unhandledRejection') to log programmer errors and exit the process cleanly, while using structured catch blocks for operational errors.

Here is how a Senior Dev implements Centralized Handling and Custom Error Classes in Express.

// 1. Define a Custom Error Class
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true; // Distinguish from programmer bugs
    Error.captureStackTrace(this, this.constructor);
  }
}

// 2. The Controller (Clean and focused)
app.get('/api/orders/:id', async (req, res, next) => {
  try {
    const order = await Order.findById(req.params.id);
    if (!order) {
      // We don't handle the response here. We pass it to the 'gatekeeper'.
      return next(new AppError('No order found with that ID', 404));
    }
    res.status(200).json(order);
  } catch (err) {
    next(err); // Pass unexpected errors to global handler
  }
});

// 3. The Global Error Middleware (The Gatekeeper)
app.use((err, req, res, next) => {
  err.statusCode = err.statusCode || 500;

  // Log to Sentry/Datadog for monitoring
  console.error(`[ERROR] ${err.message} - Stack: ${err.stack}`);

  // Send a clean, non-leaky response to the client
  res.status(err.statusCode).json({
    status: err.statusCode < 500 ? 'fail' : 'error',
    message: err.isOperational ? err.message : 'Something went very wrong!'
  });
});
Enter fullscreen mode Exit fullscreen mode

Top comments (0)