6 min read
CS Fundamentals Index
Tier 1 -- Foundations
Object-Oriented Programming
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs
CS Fundamentals Index
Tier 1 -- Foundations
Object-Oriented Programming
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs
Exceptions vs Error Values
1. What Is It?
When something goes wrong in a function — file not found, bad input, network failure — the function has to tell its caller. There are two fundamentally different ways: throw an (a separate, out-of-band channel that unwinds the until someone catches it) or return an error value (the normal return path, but the value indicates "this didn't work").
Without a clear picture of which model a language uses and when to reach for which, you get code that either silently swallows failures or wraps every call in a paranoid .
A backend API function attempts to read a configuration file at startup. If the file is missing, the function uses the exception-based error model. Which behavior best describes how this error is communicated to the caller?
2. How It Works
Exceptions
An is a special "thrown" from where the error occurred. Normal execution halts; the runtime walks up the call , discarding frames, until it finds a matching catch/except handler. If none exists, the program crashes with a trace.
# Python def read_age(s): return int(s) # raises ValueError if not numeric try: age = read_age("abc") except ValueError as e: print(f"bad input: {e}")
// Java int readAge(String s) { return Integer.parseInt(s); // throws NumberFormatException } try { int age = readAge("abc"); } catch (NumberFormatException e) { System.out.println("bad input: " + e.getMessage()); }
// C++ int read_age(const std::string& s) { return std::stoi(s); // throws std::invalid_argument } try { int age = read_age("abc"); } catch (const std::invalid_argument& e) { std::cout << "bad input: " << e.what() << "\n"; }
Error values
An error value is a regular return that encodes success or failure in its type. The caller must inspect it. Go uses the (result, err) tuple idiom; Rust uses Result<T, E>; C uses sentinel integers (-1, NULL). In Python and Java, you can simulate it with Optional, tuples, or sum types — but the native failure channel is exceptions.
# Python — error-value style (uncommon but valid) def read_age(s): if not s.isdigit(): return None return int(s) age = read_age("abc") if age is None: print("bad input")
// C++ — std::expected<T, E> (C++23) — or std::optional for yes/no #include <expected> std::expected<int, std::string> read_age(const std::string& s) { if (s.empty()) return std::unexpected("empty"); try { return std::stoi(s); } catch (...) { return std::unexpected("not a number"); } } auto r = read_age("abc"); if (!r) std::cout << "bad: " << r.error() << "\n"; else std::cout << "age: " << *r << "\n";
Stack unwinding vs. explicit propagation
With exceptions, callers who don't care about the error don't need to do anything — the sails past them. With error values, every caller must explicitly pass the error up (or handle it). That's more ceremony but more explicit.
A backend API has a deep call chain: handleRequest() → parseBody() → validateUser() → fetchRecord(). The fetchRecord() function encounters a database error. If the team uses exceptions instead of error values, what happens to the intermediate functions validateUser() and parseBody()?
3. What You Actually Need to Know
- Exceptions are for exceptional conditions, not normal control flow. Using them to signal "user chose option 2" is a classic anti-pattern — it's slow, obscures intent, and breaks traces as diagnostic tools.
- Error values force the caller to confront failure; exceptions don't. Exceptions are great until you forget to catch one and it crashes production.
Result<T, E>/Optional<T>are great until 80% of your code is error-propagation boilerplate. - Performance: throwing and catching an is expensive ( unwinding, allocation). In a hot loop, error values win. For one-off operations, the cost is negligible and the code clarity matters more.
- Java has checked exceptions (
throws SomeExceptionin the signature). The compiler enforces that callers either catch or declare them. This is controversial — some love the explicitness, others find it leads tothrowseverywhere or catch-and-ignore blocks. - Python, C++, and Java runtime exceptions are unchecked — the compiler doesn't force you to handle them. You learn about them from docs, conventions, or production crashes.
- C has no exceptions. Errors are always values (
errno, return codes, output pointers). Same for Go, Rust, older C++ codebases, and many embedded / systems contexts. - Debugging clues:
- Uncaught exception with a clean stack trace → great — the trace tells you exactly where and why.
- Missing error check → function returns a result you didn't look at; silently wrong output downstream.
catch (Exception e) {}→ swallowed error; a future developer (or you at 3am) won't know the failure happened.tryblock around 200 lines → too broad; narrow to the single operation that might fail.
A backend service processes payments in a high-throughput loop — thousands of transactions per second. The current implementation throws a custom exception whenever a transaction is declined (a common, expected outcome). A teammate suggests replacing this with an error value (e.g., a Result type or return code). What is the strongest technical reason to make this change?
4. Language Differences
| Aspect | Python | Java | C++ |
|---|---|---|---|
| Native failure channel | Exceptions | Exceptions (checked + unchecked) | Exceptions |
| Checked exceptions? | No | Yes (unless extending RuntimeException) | No |
| Error-value idioms | Optional, tuples, sentinel None | Optional<T>, explicit wrapper classes | std::optional, std::expected (C++23), error codes |
| Cost of throwing | Moderate (Python exceptions are relatively cheap) | Moderate (stack capture is the expensive part) | Can be zero-cost on success path but very expensive on throw |
| Standard practice | Exceptions | Exceptions (but Optional common for "not found") | Depends on codebase / domain — games and embedded often disable them entirely |
Go and Rust (both popular modern systems languages) chose error values as the default. Their influence is pushing C++ and Python codebases toward more Result/expected patterns where latency or clarity matter.
You are writing a high-performance C++ game engine where certain operations must complete with minimal latency. A colleague suggests using C++ exceptions for all error handling. What is the strongest performance-related argument against this approach in this context?
5. Tradeoffs & Decisions
-
Exceptions:
- Pros: normal-path code stays clean; failures can be handled far from where they happen; traces are powerful debugging aids.
- Cons: easy to forget to handle; hide control flow; slow when thrown; can leak resources if RAII /
try-finallyisn't used correctly.
-
Error values:
- Pros: failure is explicit in the signature; performance is predictable; no hidden control flow; forces the caller to decide.
- Cons: verbose — every call site needs a check or a propagation; pipelines of calls require
?(Rust), monadic chains (.flatMap), or nestedifblocks.
-
Choose by category of failure:
- Programmer error / bug (null pointer, index out of bounds): let it throw. You want the crash and the trace.
- Expected failure the caller must decide about (file not found, invalid input, network timeout): error value or a specific caught close to the call.
- Unrecoverable (out of memory, corrupted state): crash — don't try to handle it.
If you see a function with throws or catch (Exception), it usually hides a lack of thinking about what can actually go wrong. You'd choose error values when failure is routine enough to be part of the contract, exceptions when it's truly out-of-band.
A backend service reads a configuration file at startup. If the file is missing, the service cannot proceed. A teammate suggests catching this with a broad catch (Exception e) and logging a generic error. What is the better design choice and why?
6. Interview Cheat Sheet
- Exceptions use a separate unwinding channel — throw here, catch anywhere up the . Error values use the normal return channel — callers inspect the result.
- Exceptions keep the happy path clean but hide control flow; error values are explicit but verbose.
- Java has checked exceptions — the compiler enforces handling or declaration. Python and C++ exceptions are unchecked.
- Use exceptions for exceptional conditions, not control flow; they're expensive to throw and obscure intent when misused.
- Modern languages (Rust, Go) prefer error values; modern C++ offers
std::expected; modern Python often usesOptional/ tuples for expected failures. - Bugs crash; expected failures are handled. If it's a programmer error, let it throw — the trace is your friend.
Follow-ups:
- "When should you throw vs. return an error?" — Throw for exceptional, unexpected conditions. Return an error value when failure is a normal part of the function's contract.
- "What's the problem with
catch ( e)?" — It swallows every error, including bugs you wanted to hear about. Catch only what you can actually handle. - "Why do Go and Rust use error values instead of exceptions?" — Explicit failure in the type system, predictable performance, no hidden control flow — at the cost of verbosity.
Glossary History
Click dotted jargon to save explanations here.
Glossary History
Click dotted jargon to save explanations here.