Skip to content

Error Boundaries

Error boundaries are the top-level points in your application where you catch and handle all exceptions. This guide shows how to use saferaise at these boundaries.

Application Entry Point

The most common error boundary is your application's entry point:

# main.py
import saferaise

saferaise.register("myapp")

import myapp

def main():
    with saferaise.enable():
        try:
            myapp.run()
        except Exception:
            print("Fatal error")
            raise

if __name__ == "__main__":
    main()

Web Framework Middleware

For web frameworks, you typically wrap the request handler:

# entrypoint.py
import saferaise

saferaise.register("myapp")

from myapp.app import create_app

app = create_app()
# myapp/middleware.py
from saferaise import enable

class SafeRaiseMiddleware:
    def __init__(self, app):
        self.app = app

    async def __call__(self, scope, receive, send):
        with enable():
            try:
                await self.app(scope, receive, send)
            except Exception:
                # your error handling here
                raise

Manual Exception Watching with unsafe

Use unsafe() sparingly

unsafe() bypasses the normal try/except flow. Only use it at bootstrapping boundaries where a try/except would be artificial.

At boundaries where you know certain exceptions will be handled but there's no natural try/except, use unsafe():

from saferaise import enable, unsafe, raises

@raises(ValueError)
def parse_config(path: str) -> dict:
    ...

def bootstrap():
    """Bootstrap the application. Crashes are acceptable here."""
    with enable():
        with unsafe(ValueError):
            # We accept that ValueError might crash the process during bootstrap
            config = parse_config("config.yaml")
    return config

Layered Error Handling

In larger applications, you might have multiple layers of error boundaries:

from saferaise import raises

class DatabaseError(Exception): ...
class NotFoundError(DatabaseError): ...
class ConnectionLostError(DatabaseError): ...

@raises(NotFoundError, ConnectionLostError)
def get_user(user_id: int) -> dict:
    ...

@raises(ConnectionLostError)
def get_user_or_none(user_id: int) -> dict | None:
    """Wraps get_user, handling NotFoundError but propagating ConnectionLostError."""
    try:
        return get_user(user_id)
    except NotFoundError:
        return None

def handle_request(user_id: int) -> str:
    """Top-level handler that catches everything."""
    try:
        user = get_user_or_none(user_id)
        if user is None:
            return "User not found"
        return f"Hello, {user['name']}"
    except ConnectionLostError:
        return "Database unavailable"

How @raises composes

Each layer declares only the exceptions it doesn't handle. saferaise verifies the chain at runtime, ensuring no exception escapes without a handler.

Disabling in Production

Tip

Use saferaise for full validation in dev/test, and disable() to skip checks in production hot paths without removing instrumentation.

import os
import saferaise

saferaise.register("myapp")

import myapp

def main():
    ctx = saferaise.enable if os.getenv("ENV") != "production" else saferaise.disable
    with ctx():
        myapp.run()