For years, I wrapped everything in try-catch blocks. Every function, every API call, every database query. I thought that’s what defensive programming looked like.
I was wrong.
The Problem with Catching Everything
When you catch exceptions at every level, you lose context. You mask real errors. You make debugging a nightmare because you never know where things actually broke.
I spent hours tracing bugs only to find that someone had caught an exception, logged something vague like “error occurred”, and moved on.
What Changed My Mind
After a particularly nasty production incident where a database connection failure was swallowed somewhere deep in the call stack, I started being more intentional about error handling.
Now I follow three simple rules:
- Let exceptions propagate from lower layers
- Catch only at boundaries – API endpoints, message handlers, background jobs
- Always log with context – include request IDs, user IDs, anything that helps debug
The Result
My code became easier to debug. Errors bubble up with full stack traces. I know exactly where things fail. And surprisingly, my code is shorter now.
Not every error needs to be caught immediately. Sometimes the best thing you can do is let it fail loudly so you can fix it properly.
