Errors are not optional
Every program meets the real world eventually: a file that doesn't exist, a
network that's down, a user who types "twenty" into the age field. The
question is never whether things go wrong — it's who handles it, where,
and how gracefully. Error handling is what separates "works on my machine"
code from production code, and interviewers notice it instantly.
Three kinds of "wrong," with different fixes:
| Kind | Example | Caught by | Fix |
|---|---|---|---|
| Syntax error | if x > 5 missing the : | The language, before/at startup | Read the message, fix the typo |
| Runtime error (exception) | int("twenty"), dividing by zero, file missing | The running program — it crashes unless handled | This page |
| Logic error (bug) | Computing the discount as price * 1.2 | Nothing — code runs "fine," answer is wrong | Tests (Level 7) and debugging |
What an exception is
An exception is an object the runtime creates the moment an operation cannot proceed — it then abandons normal execution and travels up the call stack, looking for someone who declared they can handle it. If nobody does, the program dies with the stack trace you've already seen:
Traceback (most recent call last):
File "app.py", line 12, in <module>
age = int(user_input)
ValueError: invalid literal for int() with base 10: 'twenty'
Read stack traces bottom-up: last line = what went wrong (ValueError)
and where; the lines above = the chain of calls that led there. Learning to
read these calmly is a genuine professional skill — the trace is not an
insult, it's a map.
try / catch: declaring "I can handle this"
# Python
try:
age = int(input("Your age: ")) # risky line
except ValueError:
print("That's not a number — try again.")
// Java
try {
int age = Integer.parseInt(input);
} catch (NumberFormatException e) {
System.out.println("That's not a number — try again.");
}
// C++
try {
int age = std::stoi(input);
} catch (const std::invalid_argument& e) {
std::cout << "That's not a number — try again.\n";
}
Mechanics, same in all three:
- Code in
tryruns normally. If a line throws, the rest of thetryblock is skipped and control jumps to the matchingexcept/catch. - If nothing throws, the handler is skipped entirely.
- Catch blocks match by type —
ValueErrorhere. You can stack several handlers for different types, most specific first (the if/elif rule again).
The analogy: airbags. You don't drive expecting a crash, and airbags don't prevent crashes — they turn a fatal outcome into a recoverable one, at the place equipped to deal with it.
Raising your own
Your functions should refuse bad input rather than compute garbage:
def withdraw(account, amount):
if amount <= 0:
raise ValueError(f"amount must be positive, got {amount}")
if amount > account.balance:
raise InsufficientFundsError(account.id, amount)
account.balance -= amount
if (amount <= 0) {
throw new IllegalArgumentException("amount must be positive, got " + amount);
}
This is the encapsulation promise from OOP Basics with teeth: the object cannot be put into an invalid state, because invalid requests explode immediately, loudly, at the source — instead of corrupting data and exploding three days later somewhere confusing. Fail fast is the name of this principle, and it's a phrase worth using in interviews.
You can define your own exception types (class InsufficientFundsError(Exception))
so callers can react to your failure modes specifically.
Cleanup: finally and with
Some things must happen whether or not the code failed — closing files, releasing connections, unlocking locks:
# The manual way
f = open("data.txt")
try:
process(f)
finally:
f.close() # runs on success AND on exception
# The Pythonic way — 'with' does the finally for you
with open("data.txt") as f:
process(f) # f is guaranteed closed when this block exits, however it exits
Java's equivalent is try-with-resources
(try (var f = new FileReader(...)) { ... }); C++ does it through
RAII — destructors run automatically when scope exits, which is the same
guarantee built into the language. The shared idea: tie cleanup to scope,
not to memory — humans forget; scopes don't.
The judgment call: handle, or let it fly?
The hardest part isn't syntax — it's policy. Two rules cover most cases:
- Catch only what you can actually do something about. Can you retry,
use a default, or ask the user again? Catch it there. Can't? Let it
propagate to a layer that can — often all the way to one top-level handler
that logs it and returns a clean error (Level 7: this becomes your API's
500handler — see status codes). - Never swallow silently. The worst line in programming:
try:
save_order(order)
except Exception:
pass # ❌ the order vanished and nobody will ever know why
An empty catch block converts a loud, debuggable crash into silent data corruption. If you truly must continue, log it at minimum.
Don't use exceptions as ordinary control flow — "user not found" on a login
page isn't exceptional, it's Tuesday; return a result for it. Reserve
exceptions for can't-continue situations. (Python is culturally more
permissive here — StopIteration powers every for-loop — but the design
rule of thumb survives across languages.)
Common beginner mistakes
- Catching
Exception(everything) out of reflex. You wanted to catch the missing file; you also caught the typo in your own code that should have crashed in development. Catch the narrowest type that matches your recovery plan. - try around 200 lines. Wrap the one risky operation, not the whole function — otherwise you can't tell what failed or guarantee what state things are in.
- Using exceptions where an if will do. Checking
if amount <= 0before calling is often clearer than try/except around it. (Validation vs exception handling — both have a place.) - Printing instead of re-raising.
print("error!")then carrying on with broken state is swallowing with extra steps. - Forgetting the error message is for a future debugger (often you).
raise ValueError("bad input")vsraise ValueError(f"expected ISO date, got {raw!r}")— one of these saves an hour at 2 AM.
Interview perspective
Practice
Beginner
- Wrap the age-input example so it keeps asking until the user enters a valid number (while loop + try/except).
- Trigger and read three different exceptions on purpose: divide by zero,
index past the end of a list,
int("abc"). For each, identify the exception type name from the trace.
Intermediate
- Add
withdrawvalidation to yourBankAccountfrom OOP Basics: raise on non-positive amounts and on insufficient funds — with messages that include the offending values. Then write calling code that catches each case differently. - Write
safe_divide(a, b)that returnsa / b, but returnsNoneon division by zero. Then argue (one paragraph): when is returningNonebetter than raising, and what new bug does returningNoneinvite?
Advanced
- Create a custom
RetryableError, and write a functionwith_retries(f, attempts)that callsf, catches onlyRetryableError, retries up toattemptstimes, and re-raises after the last failure. (You wrote a higher-order function with an error policy — that's production code shape. Compare with what you'll meet in Level 7.)
Next: Collections — lists, dictionaries, sets and tuples, and how to pick between them.