How do I add context to an exception without losing the original error?
Short answer
Use raise NewError(...) from err to attach a meaningful error while keeping the original as its cause. The traceback then shows both, which is what you need to diagnose the failure.
The short version
Use raise MyError("context") from original_error. This raises your own, more meaningful exception while preserving the original as the cause, so the traceback shows both. Catching an error and raising a new one without from hides the real cause and makes debugging much harder.
Chaining properly
class ConfigError(Exception):
pass
def load_setting(text):
try:
return int(text)
except ValueError as err:
raise ConfigError(f"setting must be a whole number, got {text!r}") from err
The caller now sees a clear ConfigError explaining what was wrong with their configuration, and the traceback still shows the underlying ValueError beneath it, marked as the direct cause.
try:
load_setting("abc")
except ConfigError as err:
print(type(err.__cause__).__name__) # ValueError
Why the context helps
A bare ValueError: invalid literal for int() with base 10: 'abc' tells a user nothing about which setting was wrong or what to do. Your own exception adds the meaning. But the original error tells you, the maintainer, exactly which line failed and why, which is why you want both, not one or the other.
What happens without from
If you raise inside an except block without from, Python still shows the original error, but labels it with "During handling of the above exception, another exception occurred". That wording suggests a second, unrelated failure happened while handling the first, which is misleading when in fact you deliberately translated one into the other.
Using from changes the wording to "The above exception was the direct cause of the following exception", which is an accurate description of what you did.
Suppressing the original deliberately
Occasionally the original error is noise and would only confuse.
raise ConfigError("configuration file is unreadable") from None
from None suppresses the context entirely. Use it sparingly, and only when the underlying error genuinely adds nothing, because you are throwing away diagnostic information.
Do not swallow errors
The worst pattern is catching an exception and raising a new one with no cause, or worse, catching and returning a default.
try:
value = int(text)
except ValueError:
value = 0 # the failure is now invisible
A silent default turns a loud, fixable failure into a wrong number that flows quietly through everything downstream. If a bad value is genuinely acceptable, say so explicitly and log it; if it is not, let the error propagate.
Where this pattern belongs
Chaining is most valuable at the boundaries of your code: where you read a file, parse configuration, call an API, or query a database. There, a low-level error (a KeyError, a ValueError, a connection failure) means little to the caller, and translating it into a domain error with context is exactly the right thing, provided you keep the cause attached.