Custom Exceptions & Exception Chaining
A custom exception is your own class that subclasses Exception , giving a failure a meaningful name callers can catch precisely — and exception chaining ( raise ... from ... ) links a high-level error to the lower-level cause that triggered it.
Learn Custom Exceptions & Exception Chaining in our free Python course — an interactive lesson with runnable examples, a practice exercise and a quick…
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Robust programs don't just raise errors — they raise the right errors, organized into a hierarchy, with the original cause preserved. That's the difference between a cryptic stack trace and an error message that tells you exactly what went wrong and why.
Subclass Exception to create a named error. Group related errors under a common base so callers can catch the whole family at once or pick out a specific one:
Catching AppError grabs both subtypes; catching ValidationError grabs only that one. The hierarchy gives callers a choice of precision.
When a low-level error happens, you often want to raise a clearer, higher-level one — without losing the original. raise NewError(...) from original does exactly that, storing the original in __cause__ :
In a real traceback, chaining prints both errors joined by "The above exception was the direct cause of the following exception." You get a clean domain error and the technical root cause.
A full try statement has four clauses. else runs only on success; finally runs always; and a bare raise inside except re-raises the current exception after you've done something with it:
Sometimes several things fail at once and you want to report all of them, not just the first. Python 3.11 added ExceptionGroup and the except* syntax to raise and handle a bundle of exceptions:
Catching an exception is a commitment to handle it. If you can recover, substitute, retry, or translate it, catch it. If you can't, let it propagate — and never silence one you don't understand:
Complete the custom exception and the chaining. Replace each ___ , then run it.
✅ Subclass Exception (not BaseException ) for application errors:
✅ Catch what you can handle, and at least log the rest before re-raising with a bare raise .
✅ Use from to keep the chain explicit: raise ConfigError("bad config") from err .
Wrap json.loads so that any parse failure raises your own DataError chained from the original, while a clean value passes straight through.
Lesson complete — your error handling is professional now!
You can design custom exception hierarchies, chain errors with raise ... from ... , bundle failures with ExceptionGroup , use else and finally correctly, re-raise deliberately, and decide when catching even makes sense. Your tracebacks will tell the whole story now.
🚀 Up next: Time Zones with zoneinfo — handle aware datetimes correctly.
Practice quiz
Which base class should application exceptions subclass?
- BaseException
- RuntimeError
- Exception
- object
Answer: Exception. Subclass Exception for application errors. BaseException is too low-level and would also catch things like KeyboardInterrupt.
If ValidationError and ConfigError both subclass AppError, what does 'except AppError' catch?
- Both ValidationError and ConfigError (the whole family)
- Only AppError instances
- Nothing
- Only ValidationError
Answer: Both ValidationError and ConfigError (the whole family). Catching a base class catches all its subclasses, so 'except AppError' grabs the whole family of app errors at once.
What does 'raise NewError(...) from original' do?
- Deletes the original exception
- Ignores NewError
- Re-runs the original code
- Raises NewError while recording original as its __cause__
Answer: Raises NewError while recording original as its __cause__. The 'from' syntax chains exceptions: it raises the new error while storing the original in __cause__, preserving the root cause.
After 'raise ConfigError(...) from e', where is the original exception stored?
- e.__context__ only
- e.__cause__ of the new exception
- It is lost
- In a global variable
Answer: e.__cause__ of the new exception. The original is recorded as the new exception's __cause__, so you can inspect it and the traceback shows both errors.
When does the 'else' block of a try statement run?
- Only when the try body completed with no exception
- Always
- Only when an exception was raised
- Never
Answer: Only when the try body completed with no exception. The else clause runs only on success — when the try body raised no exception — and its own errors are not caught by the except.
When does the 'finally' block run?
- Only on success
- Only on failure
- No matter what — exception or not, even after a return
- Only if there is no except clause
Answer: No matter what — exception or not, even after a return. finally always runs, making it the right place for cleanup like closing files or releasing locks.
What does a bare 'raise' inside an except block do?
- Raises a new generic error
- Re-raises the current exception unchanged
- Silences the exception
- Returns None
Answer: Re-raises the current exception unchanged. A bare 'raise' re-raises the exception currently being handled, so you can log it and then let it propagate.
What does 'except* ValueError' (with the star) handle?
- A single ValueError
- All exceptions
- Only TypeErrors
- ValueErrors peeled from an ExceptionGroup
Answer: ValueErrors peeled from an ExceptionGroup. except* matches and handles the exceptions of a given type within an ExceptionGroup, which can bundle many exceptions together (Python 3.11+).
What is wrong with 'except Exception: pass'?
- It is a syntax error
- It silently swallows errors, hiding real bugs
- It only works once
- It re-raises automatically
Answer: It silently swallows errors, hiding real bugs. Silently swallowing exceptions hides bugs (even typos). Catch only what you can handle, and at least log the rest.
In 'load_port' that does 'raise ConfigError(...) from e' on a bad int, what is e.__cause__?
- A ConfigError
- None
- A ValueError
- A TypeError
Answer: A ValueError. int('abc') raises ValueError, which is captured as e and becomes the __cause__ of the chained ConfigError.