Error Handling
Error handling is how a program detects that something went wrong, reacts and reports it. Missing files, bad input and unreachable services are normal, so plan for them.
Two styles
- Exceptions. The failing code raises an exception, which travels up the call stack until something catches it. If nothing does, the program stops and prints a stack trace.
- Return values. The function returns an error code or a result object (for example a value plus an error) and the caller must check it. Some languages favour this style.
Either way, never ignore a failure silently.
Rules for handling errors
- Handle only what you can fix. Catch an error where you know what to do about it (retry, use a default, ask the user). Otherwise let it travel upward.
- Catch specific errors. Catching everything also catches bugs you did not expect.
- Never swallow an error. An empty catch block makes the program fail later, somewhere else, with no clue why.
- Add context. “Could not read config file /etc/app.json: permission denied” beats “error”.
- Fail fast. Check inputs at the boundary and stop early, so bad data goes no deeper. See Input Validation.
- Clean up. Close files, connections and locks even when something fails (
finally,with,using,defer, depending on the language). - Fail safely. When in doubt, deny access or stop instead of continuing in an unknown state.
# Python
try:
with open(path) as f:
settings = json.load(f)
except FileNotFoundError:
settings = DEFAULT_SETTINGS # a sensible, expected fallback
except json.JSONDecodeError as e:
raise ValueError(f"Invalid JSON in {path}: {e}") from e # add context, keep the cause
Two audiences
The user needs a short, safe message and a next step. The developer needs the details: what failed, where, with which inputs and the stack trace. Show the first to the user and write the second to the logs. Do not show stack traces, SQL or file paths to end users.
Retries, timeouts and expected errors
- Retries suit temporary faults such as a network blip. Use a limit and a growing delay, and only retry operations that are safe to repeat (idempotent).
- A timeout stops one slow dependency from hanging everything.
- A user typing a bad date is expected and needs a friendly message. A null reference is a bug and needs a fix.
Try it
Write a function that reads a number from a file. List every way it could fail (missing file, empty file, text instead of a number). Handle each with a clear message, then check your messages by triggering each failure on purpose.
Common mistakes
- An empty catch block that swallows the error.
- Catching everything, which also catches unexpected bugs.
- Showing stack traces, SQL or file paths to end users.
- Retrying operations that are not safe to repeat, or retrying without a limit.
Check yourself
- When should you catch an error and when should you let it travel upward?
- What belongs in the user message and what in the log?
- Which operations are safe to retry?