Skip to content

Programming Fundamentals & OOP

Error Handling

Error handling is the set of language features used to detect a failure while a program is running and respond to it instead of letting the whole program crash. In Python, and most mainstream languages, this is built around exceptions: an operation that cannot complete raises an exception, which interrupts normal execution and searches outward for a handler willing to catch it. Good error handling distinguishes failures a program can reasonably recover from, such as a missing file or bad user input, from genuine programmer bugs, which usually should not be silently swallowed.

Why it matters

Real programs touch things outside their control
Files might not exist, networks time out, users type the wrong thing - these are certainties over a program's lifetime, not edge cases to consider later.
An unhandled exception ends the whole program, or the whole request
In a web server where one unhandled exception can take down a request other users depend on, handling narrows the blast radius to where the failure actually happened.
Catching too broadly hides real bugs instead of fixing them
A bare except that silently continues turns a loud, easy-to-find crash into a quiet, much harder to diagnose one, often discovered far later and far from its cause.
Clear error handling and messages are often the difference between a five-minute fix and an hour of guessing
A specific exception type and message tell you exactly what failed and often why; a generic failure notice does not.

Exceptions: raising and catching

A try block wraps code that might fail; one or more except blocks after it catch specific exception types and run recovery code instead of letting the program crash. else runs only if the try block succeeded with no exception, and finally runs regardless of whether an exception happened, which makes it the right place for cleanup that must always occur. When code raises an exception, execution jumps immediately out of the try block to the first except clause that matches its type - any remaining lines in the try block never run.

Python
try:
    age = int(input('Age: '))
except ValueError:
    print("That wasn't a number")
else:
    print(f'You are {age} years old')
finally:
    print('Done asking')

Being specific about what you catch

A bare except with no type, or except Exception, catches essentially everything, including mistakes that were never meant to be recoverable. A bare except that silently passes is one of the most common real-world sources of bugs that take a long time to find, because the program keeps running as if nothing happened, with no trace of what actually failed. Catching a specific type, such as FileNotFoundError or ValueError, only intercepts the failure you actually anticipated and planned a response for, and lets anything else propagate - which is usually the more honest behavior, since an unanticipated failure deserves a visible crash, not a silent skip.

Python
try:
    with open('settings.json') as f:
        data = f.read()
except FileNotFoundError:
    data = '{}'   # fall back to an empty config, a genuinely expected case
# any other kind of failure (permissions, disk error) is still raised, not hidden

Raising your own errors

A function can raise an exception deliberately with raise, to signal that its caller passed something invalid or that a precondition was not met, rather than returning a value that only looks valid. Raising a built-in type like ValueError, or a custom exception class that is usually a small subclass of Exception, documents exactly what went wrong at the point closest to the actual cause, instead of letting an invalid value travel further into the program and fail somewhere much harder to trace back.

Mistakes people make here

Catching everything and silently passing
A bare except that does nothing turns a loud, visible crash into a silent, much harder to find bug, because the program keeps running as if the failure never happened, with no record left behind of what it actually was.
Using exceptions for ordinary control flow where a plain check reads more clearly
Python does idiomatically use try/except for some genuinely expected outcomes, a style sometimes summarized as easier to ask forgiveness than permission, but reaching for an exception to handle a case a simple if could check just as clearly adds indirection without benefit.
Not cleaning up a resource when an error happens mid-operation
A file or connection opened before an exception is raised can be left open if there is no finally block or context manager guaranteeing cleanup. A with statement, or a finally block, ensures the resource is released whether or not the operation succeeded.
Re-raising or wrapping an exception without preserving what actually caused it
Catching an exception and raising a different, more generic one in its place, without keeping a reference to the original, throws away the exact information that would have made debugging fast. Python's raise ... from ... syntax keeps that link intact.

Strengths and trade-offs

Where it is strong

  • Separates the normal-case logic from failure-handling logic, so the common path stays readable instead of being interleaved with checks.
  • A specific exception type documents exactly what can go wrong, in a way a boolean success flag cannot.
  • A finally block or context manager guarantees cleanup code runs even when something fails partway through an operation.

The trade-offs

  • Exceptions carry a real runtime cost in some languages and situations, and can be slower than a simple conditional check when used inside a performance-sensitive loop.
  • Overly broad catches trade a visible crash for a silent, much harder to find bug - the crash was, in a real sense, the more honest outcome.
  • Deciding what genuinely counts as an exceptional condition versus an expected outcome is a real design judgment call that different codebases answer differently and not always consistently.

Who needs this

Anyone shipping code that runs outside a fully controlled tutorial environment - reads a file, calls a network, or takes user input, all of which fail in the real world with some regularity.

Questions about error handling

What's the difference between an error and an exception?
In everyday use they are often interchangeable, but more precisely: an error is the underlying problem, such as a file being missing, and an exception is the language mechanism, an object carrying information about that problem, used to signal and handle it at run time.
Why is a bare except that does nothing considered bad practice?
Because it silences every possible failure, including ones you never anticipated and have no actual recovery for, and leaves no trace that anything went wrong. The program keeps running in a state the code never actually accounted for.
Should I use exceptions or return codes to signal failure?
It depends on the language's conventions more than on a universal rule. Python's own standard library and idioms lean heavily on exceptions; some other ecosystems favor explicit return values or result types. Within a codebase, consistency with its existing convention matters more than which approach is abstractly better.
What does finally guarantee that code placed after a try/except does not?
finally runs whether the try block succeeded, failed and was caught, or failed and was not caught at all, including when an exception is about to propagate further up. Code simply placed after the whole try/except block would be skipped in that last case, since the exception would already be on its way out.

The primary source

Related concepts

← All concept guides