Skip to content

Exceptions

Exceptions let you handle errors consistently across your application. Ravyn provides built-in exceptions for common scenarios and makes it easy to create custom ones.

What You'll Learn

  • Built-in Ravyn exceptions and when to use them
  • Creating custom exceptions
  • Raising exceptions with helpful error messages
  • Using ValidationError for clean error responses

Quick Start

from ravyn import Ravyn, get
from ravyn.exceptions import NotFound, PermissionDenied

@get("/users/{user_id}")
def get_user(user_id: int) -> dict:
    user = find_user(user_id)  # Your database lookup

    if not user:
        raise NotFound("User not found")

    if not user.is_active:
        raise PermissionDenied("User account is inactive")

    return {"user": user}

app = Ravyn()
app.add_route(get_user)

When raised, these exceptions automatically return proper HTTP status codes and JSON responses.


Built-In Exceptions

All Ravyn exceptions inherit from HTTPException and return JSON error responses.

Common Exceptions

Exception Status Code Use Case
NotFound 404 Resource doesn't exist
NotAuthenticated 401 User not logged in
NotAuthorized 401 Authentication failed
PermissionDenied 403 User lacks permission
ValidationError 400 Invalid input data
MethodNotAllowed 405 Wrong HTTP method
InternalServerError 500 Server error
ServiceUnavailable 503 Service down
ImproperlyConfigured 500 Configuration error

Import Exceptions

from ravyn.exceptions import (
    NotFound,
    NotAuthenticated,
    PermissionDenied,
    ValidationError,
    HTTPException
)

Exception Details

NotFound (404)

Raise when a resource doesn't exist:

from ravyn import get
from ravyn.exceptions import NotFound

@get("/posts/{post_id}")
def get_post(post_id: int) -> dict:
    post = database.get_post(post_id)

    if not post:
        raise NotFound(f"Post {post_id} not found")

    return {"post": post}

NotAuthenticated (401)

Raise when user needs to log in:

from ravyn import get, Request
from ravyn.exceptions import NotAuthenticated

@get("/profile")
def get_profile(request: Request) -> dict:
    user = request.user  # From authentication middleware

    if not user:
        raise NotAuthenticated("Please log in to view your profile")

    return {"user": user}

PermissionDenied (403)

Raise when user lacks permission:

from ravyn import delete
from ravyn.exceptions import PermissionDenied

@delete("/posts/{post_id}")
def delete_post(post_id: int, user: User) -> dict:
    post = database.get_post(post_id)

    if post.author_id != user.id:
        raise PermissionDenied("You can only delete your own posts")

    database.delete(post)
    return {"deleted": True}

ValidationError (400)

Special exception for clean validation error responses:

from ravyn import post
from ravyn.exceptions import ValidationError
from pydantic import BaseModel, model_validator

class PasswordChange(BaseModel):
    password: str
    confirm_password: str

    @model_validator(mode="after")
    def passwords_match(self):
        if self.password != self.confirm_password:
            raise ValidationError({"confirm_password": "Passwords do not match"})
        return self

@post("/change-password")
def change_password(data: PasswordChange) -> dict:
    return {"success": True}

Response when validation fails:

{
  "detail": {
    "confirm_password": "Passwords do not match"
  }
}

ValidationError Formats

ValidationError accepts multiple formats:

from ravyn.exceptions import ValidationError

# String
raise ValidationError("Invalid input")

# Dict (recommended for field-specific errors)
raise ValidationError({"email": "Email already exists"})

# List
raise ValidationError(["Error 1", "Error 2"])

# Tuple
raise ValidationError(("Error 1", "Error 2"))

# Custom status code
raise ValidationError("Unauthorized", status_code=401)

InternalServerError (500)

Used for server errors. Shows detailed traceback if debug=True:

from ravyn import get
from ravyn.exceptions import InternalServerError

@get("/risky")
def risky_operation() -> dict:
    try:
        result = perform_complex_operation()
        return {"result": result}
    except Exception as e:
        raise InternalServerError(f"Operation failed: {str(e)}")

ImproperlyConfigured (500)

Raised when application is misconfigured:

from ravyn.exceptions import ImproperlyConfigured

if not settings.secret_key:
    raise ImproperlyConfigured("SECRET_KEY must be set in settings")

Creating Custom Exceptions

Create exceptions for your specific use cases:

from ravyn import HTTPException

class PaymentRequired(HTTPException):
    status_code = 402
    detail = "Payment required to access this resource"

class RateLimitExceeded(HTTPException):
    status_code = 429
    detail = "Too many requests"

class InvalidAPIKey(HTTPException):
    status_code = 401
    detail = "Invalid API key provided"

Using Custom Exceptions

from ravyn import get

@get("/premium-content")
def premium_content(user: User) -> dict:
    if not user.has_subscription:
        raise PaymentRequired("Subscribe to access premium content")

    return {"content": "Premium data"}

@get("/api/data")
def api_endpoint(api_key: str) -> dict:
    if not validate_api_key(api_key):
        raise InvalidAPIKey()

    return {"data": "..."}

Exception Handlers

Handle exceptions globally with custom handlers. See Exception Handlers for details.

Quick Example

from ravyn import Ravyn
from ravyn.exceptions import NotFound
from ravyn.responses import JSONResponse

def handle_not_found(request, exc):
    return JSONResponse(
        {"error": "Resource not found", "detail": str(exc)},
        status_code=404
    )

app = Ravyn(
    exception_handlers={
        NotFound: handle_not_found
    }
)

Common Pitfalls & Fixes

Pitfall 1: Not Providing Helpful Messages

Problem: Generic error messages aren't helpful.

# Not helpful
@get("/users/{user_id}")
def get_user(user_id: int) -> dict:
    user = find_user(user_id)
    if not user:
        raise NotFound()  # Generic message

Solution: Provide specific, actionable messages:

# Helpful
@get("/users/{user_id}")
def get_user(user_id: int) -> dict:
    user = find_user(user_id)
    if not user:
        raise NotFound(f"User with ID {user_id} does not exist")

Pitfall 2: Using Wrong Exception Type

Problem: Using NotAuthenticated when PermissionDenied is more appropriate.

# Wrong exception type
@delete("/posts/{post_id}")
def delete_post(post_id: int, user: User) -> dict:
    post = get_post(post_id)
    if post.author_id != user.id:
        raise NotAuthenticated()  # User IS authenticated, just not authorized

Solution: Use the correct exception:

# Correct exception type
@delete("/posts/{post_id}")
def delete_post(post_id: int, user: User) -> dict:
    post = get_post(post_id)
    if post.author_id != user.id:
        raise PermissionDenied("You can only delete your own posts")

Pitfall 3: Catching Exceptions Without Re-Raising

Problem: Swallowing exceptions silently.

# Exception swallowed
@get("/data")
def get_data() -> dict:
    try:
        data = fetch_from_api()
        return {"data": data}
    except Exception:
        return {}  # Error hidden from user!

Solution: Re-raise or return proper error:

# Proper error handling
@get("/data")
def get_data() -> dict:
    try:
        data = fetch_from_api()
        return {"data": data}
    except ConnectionError as e:
        raise ServiceUnavailable(f"External API unavailable: {str(e)}")
    except Exception as e:
        raise InternalServerError(f"Failed to fetch data: {str(e)}")

Pitfall 4: ValidationError with Wrong Format

Problem: Using ValidationError incorrectly.

# Not structured for easy parsing
raise ValidationError("Email is invalid and password is too short")

Solution: Use dict format for field-specific errors:

# Structured and parseable
raise ValidationError({
    "email": "Invalid email format",
    "password": "Password must be at least 8 characters"
})

Exception Response Format

All Ravyn exceptions return JSON in this format:

{
  "detail": "Error message here"
}

For ValidationError with dict:

{
  "detail": {
    "field_name": "Error message",
    "another_field": "Another error"
  }
}

Best Practices

1. Be Specific

# Vague
raise NotFound("Not found")

# Specific
raise NotFound(f"Product with SKU '{sku}' not found")

2. Use Appropriate Status Codes

# Wrong status code
raise ValidationError("Unauthorized", status_code=500)

# Correct status code
raise NotAuthorized("Invalid credentials")

3. Provide Actionable Information

# Not actionable
raise PermissionDenied("Access denied")

# Actionable
raise PermissionDenied("You need 'admin' role to perform this action")

Next Steps

Now that you understand exceptions, explore: