Custom exceptions & EAFP

RoadmapsPython Backend

Overview

Custom exceptions are your own error classes created by inheriting from Python's built-in `Exception` class. By convention, their names end in `Error` (e.g. `UserNotFoundError`). You can add custom attributes to them, such as `status_code` or `user_id`, allowing the exception to carry structured data about the failure up to the caller. A common professional pattern is to create one base exception for your application (`class MyAppError(Exception): pass`), and have all specific errors inherit from it.

Built-in exceptions like `ValueError` are too generic. If your service catches a `ValueError`, did it happen because a user provided bad input, or because a database configuration was malformed? Custom exceptions create a domain-specific vocabulary. By raising `InsufficientFundsError`, the caller can catch EXACTLY that error and trigger a specific UI flow, without accidentally catching unrelated standard library errors.

Where used: Building SDKs so users can catch specific API errors, Domain-driven design to express business logic failures, Mapping specific exceptions to HTTP 400/404/500 responses in FastAPI

Why learn this

Code walkthrough

class PaymentError(Exception):
    def __init__(self, message, code):
        super().__init__(message)
        self.code = code

try:
    raise PaymentError("Declined", code=402)
except PaymentError as e:
    print(f"Failed: {e} (HTTP {e.code})")

Focus: The custom `__init__` allows the exception to carry a specific `code` attribute, turning the error into a structured data carrier.

Common mistakes

Glossary

inheriting
A way for a new class to take on the properties and behaviors of an existing class, e.g. `class CustomError(Exception):`.
traceback
A detailed report showing the sequence of function calls that led to an error in your code, e.g. `Traceback (most recent call last):`.

Recall questions

Understanding checks

What will be printed when this code is executed?

Caught AppError

Because `AuthError` inherits from `AppError`, the `except AppError` block matches the exception first. When catching exceptions, you must put the most specific subclasses before the more general base classes.

A junior developer wants to ensure their custom exception `FatalSystemError` cannot be caught by accidental `except Exception:` blocks, so they inherit from `BaseException`. What is the danger of this?

Inheriting from `BaseException` means typical `try/except Exception:` blocks won't catch it, potentially bypassing cleanup logic or global error handlers.

`BaseException` is the root of all exceptions in Python, intended only for system-exiting exceptions like `KeyboardInterrupt` or `SystemExit`. Inheriting from it breaks the assumption made by most frameworks and libraries that all standard errors subclass `Exception`.

Practice tasks

Challenge

Build a custom validation error

Create an `InvalidEmailError` that takes an invalid email string. Write `validate_email(email)` that raises it if there's no '@'. Catch it and print its message.

Continue learning

Previous: try / except / else / finally

Return to Python Backend Roadmap