A developer's framework for categorizing errors into expected (recoverable, user-caused) and unexpected (bugs, developer fault) types, with implications for how we should handle each category in software design.
The Two Kinds of Error: A Pragmatic Approach to Error Handling
Error handling is one of those aspects of programming that often gets overlooked until something goes wrong. After years of building web applications and software systems, I've developed a mental model that has served me well: errors fall into two distinct categories, and understanding this distinction can dramatically improve both your code quality and user experience.
Expected Errors: The Normal Part of Operation
Expected errors happen during the normal course of your program's operation. These are situations where something goes wrong that isn't the developer's fault, and there's often little you could have done to prevent it. Here are some classic examples:
- Validation errors: When a user enters invalid data into a form, you can't control what they type. This is normal operation, not a bug.
- Network failures: If a user's connection drops or is slow, that's outside your control.
- Permission issues: You can't magically read files you don't have access to or use features the user hasn't granted permission for.
These errors aren't bugs—they're simply part of how your software interacts with the real world. The developer hasn't made a mistake when these occur.
The key insight: Expected errors are recoverable. You should handle them gracefully by:
- Logging a warning or info message
- Showing a helpful message to the user
- Using fallback values or alternative approaches
Crucially, expected errors should not throw, raise, or panic. Instead, they should return an error result. This pattern varies by language but often takes the form of:
- Result types (like Rust's
Result<T, E>) - Nullable values combined with success values
- Error codes
This approach forces you to think about error handling and makes your software more reliable. You might even want to set up alerts if you start getting lots of warnings, as this could indicate a systemic issue.
Unexpected Errors: The Developer's Responsibility
Unexpected errors should never happen. If they do, you've got a bug somewhere in your code. These include:
- Assertion failures: When a function's contract is violated (e.g., receiving an empty string when you required a non-empty one)
- Logic errors: When dependencies aren't properly initialized
- Null pointer exceptions: Classic examples of things going wrong unexpectedly
- Invalid data from trusted sources: If your database returns malformed data, something's broken
The radical approach: For unexpected errors, I often think the best response is to completely crash the program. Yes, it's disruptive in the short term, but this philosophy makes software feel more reliable in the long run. When your program crashes on unexpected errors:
- You're more likely to hear about these problems from users
- You're forced to fix the underlying bugs
- The alternative—continuing in an undefined state—is often worse
Unexpected errors should use ERROR or FATAL log messages because they indicate real problems that need fixing.
Drawing the Line: Context Matters
The distinction between expected and unexpected errors depends heavily on your context:
- Quick scripts and prototypes: I reckon all errors are unexpected. Who cares if a network request fails in a one-off data processing script?
- Critical systems: For something like a space probe on a 50-year mission, almost all errors become expected, including catastrophic hardware failures.
- Most applications: You'll need to decide which errors are unexpected based on your specific use case.
For example, are memory allocation errors expected in your program? It depends. In a Node.js app, you might initially think they're unexpected—until you realize that in a long-running service, they're actually quite possible and should be handled.
Language Philosophy and Error Handling
Different programming languages embody different philosophies about errors:
- Stricter languages (Rust, Zig): Classify many errors as expected, forcing you to handle them at compile time
- Looser languages (JavaScript, Python): Often classify errors as unexpected, letting them bubble up as exceptions
When parsing JSON in Go, the compiler makes you handle the error; not so in Ruby. I tend to prefer stricter compilers for production software and looser languages for scripts and prototypes, partly because of their error philosophies.
Why This Matters
Categorizing errors this way has several benefits:
- Better user experience: Users get helpful feedback instead of cryptic error messages
- More reliable software: You're forced to think about failure modes
- Easier debugging: Unexpected errors crash loudly, making bugs obvious
- Clearer code: The distinction helps you reason about control flow
This approach is very similar to Rust's error philosophy, which is no coincidence—Rust was designed with these principles in mind.
The bottom line: If you want to make your software more reliable, trend toward expecting more errors. The real world is messy, and robust software acknowledges that. Think about which errors are truly unexpected in your context, handle the rest gracefully, and don't be afraid to let bugs crash your program—it's often the first step toward fixing them.
Comments
Please log in or register to join the discussion