Error Handling
Deciding what a program does when something goes wrong: raise, return, retry, or stop — and always clean up.
Level: intermediate
What is it?
Programs meet bad input, missing files, and dead networks. Error handling decides what happens then. There are two broad styles: exceptions, which unwind the call stack to the nearest handler (Python, Java, JavaScript), and error values, which functions return and callers must check (C, Go, Rust's Result). Both need the same discipline: fail loudly, release resources, and never swallow an error you cannot handle.
Why does it matter?
Real programs spend as much code on failure as on success. Handling errors well is what separates a script from a service: users get a clear message, data stays consistent, and the on-call engineer can see what happened.
Mental model
A building's fire drill. You do not plan for the fire to happen, but you decide in advance where the alarms are, who gets told, and which doors open — so a failure follows a script instead of a panic.
How it works
- Validate at the boundary: check input where it enters the system and give a specific message.
- Catch only what you can handle. A bare except that logs nothing hides bugs.
- Release resources on every path: use with in Python, try-with-resources in Java, defer in Go.
- Add context when re-raising: what were you doing, with which input?
- Distinguish expected failures (file not found, invalid input) from bugs (a broken invariant). Recover from the first, crash on the second.
- Retries need limits and back-off, and only make sense for idempotent operations.
Steps
- Detect the problem
- Raise or return an error
- Unwind to a handler
- Clean up resources
- Recover, retry, or report
Code example: Validate, raise with context, always close
def read_port(text):
try:
port = int(text)
except ValueError:
raise ValueError(f"port must be a number, got {text!r}") from None
if not 1 <= port <= 65535:
raise ValueError(f"port out of range: {port}")
return port
with open("config.txt") as f: # closed even if an error occurs
print(read_port(f.readline().strip()))Real-world usage
- Input validation in APIs and forms
- Handling I/O and network failures
- Transactions that must roll back when a step fails
Common mistakes
- Swallowing an exception with an empty except block.
- Catching the broadest exception type and continuing as if nothing happened.
- Leaking a file, socket, or lock on the error path.
Interview questions
Exceptions versus error return codes: what are the trade-offs?
Exceptions keep the success path clean and cannot be silently ignored, but hide control flow and can be costly. Return values make every failure visible at the call site but clutter code and are easy to forget to check.
What does finally (or with) guarantee?
That cleanup code runs whether the block finishes normally, returns early, or raises. It is how you guarantee files, locks, and connections are released.
When is it safe to retry a failed operation?
When the operation is idempotent — repeating it has the same effect as doing it once — and the failure is transient. Use a limit and exponential back-off, or you turn an outage into a stampede.