Home/Learn/Python A–Z/Raising, Custom Exceptions & Chaining

Raising, Custom Exceptions & Chaining

Intermediate
Errors & Files

raise 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.

A domain vocabulary for failures
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 domain

Chaining 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.

raise X from e — tracebacks that tell the full story
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 up

Key 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 Aria
1

Why define custom exception classes instead of raising Exception("msg") everywhere?

MediumRazorpay
2

What does raise ... from do, and what appears in the traceback?

HardThoughtworks

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.

Loading discussion…