The Fallacy of the Try-Catch: Architecting for Reliable Failure
Traditional error handling often hides the true complexity of your system. We explore why treating errors as values leads to more resilient, readable, and maintainable software.

Most developers view error handling as an afterthought—a necessary evil to be "caught" and logged. We sprinkle try-catch blocks throughout our code like salt, hoping we've covered the cases that could go wrong.
But there is a fundamental flaw in this approach. Exceptions are exactly that: exceptional. They represent a break in the linear flow of logic, making it difficult to reason about the system's state. When a function throws, it effectively hiddenly returns a completely different type of data than what is in its signature.
If we want to build truly reliable software, we need to stop catching errors and start modeling them.
1. The Hidden Cost of Exceptions
Exceptions are a form of GOTO statement. They trigger a jump in the execution flow that bypasses the normal return path. This has several negative consequences:
- Invisible Failure Modes: A function signature like
getUser(id: string): Useris a lie if it can also "return" aDatabaseConnectionError. You are forced to check the implementation (or documentation) to know what to catch. - Architectural Fragility: Exceptions encourage a "log and pray" mentality. Instead of handling the specific failure locally, we often let it bubble up to a global handler, losing the context required to make an intelligent recovery decision.
- Cognitive Debt: As discussed in my post on Developer Velocity, every invisible failure mode increases your mental overhead. You have to remember which functions throw and which don't, leading to a "defensive programming" mess.
2. Errors as First-Class Citizens
The functional programming community has long had a solution to this: the Result or Either type. Instead of throwing, a function returns a container that holds either the successful value or the error.
type Result<T, E> = { ok: true, value: T } | { ok: false, error: E };
function getUser(id: string): Result<User, UserNotFoundError> {
const user = db.find(id);
if (!user) return { ok: false, error: new UserNotFoundError(id) };
return { ok: true, value: user };
}
By making the error part of the return type, we gain Type-Safe Exhaustive Checking. The compiler now requires you to handle the error case before you can access the success value. This isn't just a technical shift; it's a psychological one. It moves error handling from a peripheral concern to a core part of the feature development.
3. Railway Oriented Programming
Once you treat errors as values, you can use a pattern popularized by Scott Wlaschin called Railway Oriented Programming (ROP).
Imagine your code as a set of tracks. On the "Success Track," energy flows normally. If any step fails, the switch is flipped, and the data is diverted to the "Failure Track." The rest of the success steps are skipped, and the error is carried to the end.
This approach allows you to compose complex logic as a linear chain of operations:
const result = await pipe(
validateInput(input),
chain(updateDatabase),
chain(sendEmailNotification)
);
This drastically reduces the Architect's Ego by forcing us to keep our logic simple and composable. We aren't building complex branching trees of if/else or try/catch; we are building pipelines.
4. Building for Resilience
The goal of software Minimalism isn't just to write fewer lines of code; it's to write code that is inherently more robust.
When you architect for reliable failure:
- Your code becomes self-documenting: The types tell the whole story.
- Debugging becomes deterministic: You can trace exactly where the "switch" happened.
- Unit testing becomes comprehensive: You are forced to test the failure paths because they are just regular return values.
Stop treating errors as interruptions. Treat them as data. Your future self (and your users) will thank you.
For more on building systems that respect the limits of the human mind, read about Cognitive Load Theory and The Lindy Effect.
