Raising, Custom Exceptions & Chaining
Intermediateraise signals failure; custom exception classes give your domain a vocabulary; raise...from chains causes so tracebacks tell the whole story.
Overview
Libraries and services define their own exception hierarchy — a base error class plus specific subclasses — so callers can catch exactly the failures they care about (except PaymentError catches all payment problems; except CardDeclined catches one). raise X from e preserves the original cause in the traceback, and re-raising with a bare raise inside except lets you log-and-propagate. This is the difference between a script and a maintainable service.
Custom Exception Hierarchies
Inherit from Exception (never BaseException). A base class per module/domain lets callers choose their granularity. Attach useful fields — error handling code will thank you.
class PaymentError(Exception):
"""Base for all payment failures."""
class CardDeclined(PaymentError):
def __init__(self, card_last4, reason):
super().__init__(f"card *{card_last4} declined: {reason}")
self.card_last4 = card_last4
self.reason = reason
class GatewayTimeout(PaymentError):
pass
def charge(card, amount):
if amount > 50_000:
raise CardDeclined(card[-4:], "limit exceeded")
return "ok"
try:
charge("4242424242424242", 99_999)
except CardDeclined as e:
print("specific:", e.reason) # handle precisely
except PaymentError:
print("some other payment issue") # catch-all for the domainChaining with raise ... from & Re-raising
Wrapping a low-level error? Chain it: the traceback shows BOTH ("The above exception was the direct cause..."). Inside except, a bare raise re-raises the current exception — perfect for log-and-propagate.
class ConfigError(Exception):
pass
def load_port(raw):
try:
return int(raw)
except ValueError as e:
# translate low-level error into domain error, KEEP the cause
raise ConfigError(f"PORT must be a number, got {raw!r}") from e
try:
load_port("eighty")
except ConfigError as e:
print(e) # PORT must be a number, got 'eighty'
print(e.__cause__) # invalid literal for int() ... — original!
# Log-and-propagate — don't swallow what you can't handle
def process(order):
try:
charge(order["card"], order["amount"])
except PaymentError:
print("payment failed, alerting ops...") # side-effect
raise # SAME exception continues upKey Points to Remember
- 1Define a base exception per domain; subclass for specific failures
- 2raise NewError(...) from original preserves the causal chain in tracebacks
- 3Bare raise inside except re-raises the active exception (log-and-propagate)
- 4Inherit from Exception, not BaseException (that would break Ctrl+C)
Interview Questions
Sign in to ask AriaWhy define custom exception classes instead of raising Exception("msg") everywhere?
What does raise ... from do, and what appears in the traceback?
Ask Aria about Raising, Custom Exceptions & Chaining
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.