Exceptions vs Error Values

6 min read

Reading Progress0%
CS Fundamentals Index
Tier 1 -- Foundations
Tier 2 -- Core Concepts
Tier 3 -- Debugging & Tradeoffs
CS Fundamentals Index
Tier 1 -- Foundations
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 .

QUICK CHECK

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?

Choose one answer

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.

QUICK CHECK

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()?

Choose one answer

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 SomeException in the signature). The compiler enforces that callers either catch or declare them. This is controversial — some love the explicitness, others find it leads to throws everywhere 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.
    • try block around 200 lines → too broad; narrow to the single operation that might fail.
QUICK CHECK

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?

Choose one answer

4. Language Differences

AspectPythonJavaC++
Native failure channelExceptionsExceptions (checked + unchecked)Exceptions
Checked exceptions?NoYes (unless extending RuntimeException)No
Error-value idiomsOptional, tuples, sentinel NoneOptional<T>, explicit wrapper classesstd::optional, std::expected (C++23), error codes
Cost of throwingModerate (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 practiceExceptionsExceptions (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.

QUICK CHECK

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?

Choose one answer

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-finally isn'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 nested if blocks.
  • 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.

QUICK CHECK

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?

Choose one answer

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 uses Optional / 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.