Let's start with a confession: my first attempt at error handling in Express was... well, let's just show it:
Yes, really. We've all been there. Then it evolved into something slightly more structured, but still not perfect:
This approach had several problems:
No error typing - every error was treated the same
Inconsistent error responses across endpoints
Duplicate try-catch blocks everywhere
No proper logging (console.log doesn't count!)
No way to handle async errors properly
No context about what actually went wrong
Enter express-async-errors: The First Step
Before we dive into custom error types and middleware, we need to talk about express-async-errors. This tiny but powerful package is crucial for proper Express error handling:
Why is this important? By default, Express can't handle errors in async functions - they just disappear into the ether. This package ensures that async errors are properly caught and passed to your error handler. It's like giving Express a safety net for all those Promise rejections.
Without it, you'd need to wrap every async route handler in try-catch blocks or use .catch(next). With it, you can write clean, async code and let errors bubble up naturally:
Custom Error Types: Making Sense of Chaos
First, I needed to define what errors could actually occur. This meant creating a proper type system for errors:
Then I created a suite of error creators - functions that would generate consistently structured errors:
Type Guards: Making TypeScript Happy
To safely handle these errors, I needed type guards:
The Global Error Handler
The real magic happens in the global error middleware. Instead of handling errors in each controller, we can let them bubble up to a single handler:
Before and After: The Beauty of Clean Error Handling
Let's look at how this improved my controller code. Here's the before:
And here's the after:
The difference is striking. The new version:
Is significantly shorter
Focuses on the happy path
Has consistent error handling
Maintains type safety
Still logs everything we need
Putting It All Together
To make this work, I needed a few pieces:
Install express-async-errors to handle async errors:
Register the error handler after all routes:
Update my services to use the error creators:
What I Actually Learned
Centralized error handling is worth the initial setup time
Type safety in error handling prevents entire categories of bugs
Clean error handling makes code more readable and maintainable
Consistent error structures make API responses more predictable
Environment-aware error details improve security
Good error handling and good logging go hand in hand
Looking Forward
While this system works well, there's always room for improvement:
Adding error boundaries for the React frontend
Implementing retry logic for transient failures
Adding error tracking (probably Sentry)
Building better error reporting dashboards
But for now, I have a clean, type-safe error handling system that makes debugging easier and keeps my controllers focused on their primary responsibilities.
Next up: Refactoring the entire backend test suite using supertest!