Skip to content

Error Handling in sillo

sillo provides a robust and flexible error handling system that allows you to manage exceptions gracefully and return appropriate responses to clients. This documentation covers all aspects of error handling in sillo applications.

sillo provides a robust and flexible error handling system that allows you to manage exceptions gracefully and return appropriate responses to clients. This documentation covers all aspects of error handling in sillo applications.

from sillo.exceptions import HTTPException
@app.get("/users/{user_id}")
async def get_user(request, response, user_id: int):
user = await find_user(user_id)
if not user:
raise HTTPException(detail="User not found", status = 404)
return response.json(user)

If you raise a non-HTTPException (e.g., raise ValueError(“fail”)), sillo will return a 500 Internal Server Error unless you register a handler for that exception type.

sillo includes built-in HTTP exceptions for common error scenarios:

from sillo.exceptions import HTTPException
@app.get("/users/{user_id}")
async def get_user(request, response, user_id: int):
user = await find_user(user_id)
if not user:
raise HTTPException(detail="User not found", status = 404)
return response.json(user)
# If you raise a non-HTTPException (e.g., raise ValueError("fail")), sillo will return a 500 Internal Server Error unless you register a handler for that exception type.

All HTTP exceptions accept these parameters:

  • status_code: HTTP status code (required for base HTTPException)

  • detail: Error message or details

  • headers: Custom headers to include in the response

raise HTTPException(
status_code=400,
detail="Invalid request parameters",
headers={"X-Error-Code": "INVALID_PARAMS"}
)

sillo provides a way to create custom exception classes that extend the built-in HTTPException class. This allows you to define specific error handling behavior for specific types of errors.

from sillo.exceptions import HTTPException
class PaymentRequiredException(HTTPException):
def __init__(self, detail: str = None):
super().__init__(
status_code=402,
detail=detail or "Payment required",
headers={"X-Payment-Required": "true"}
)
@app.get("/premium-content")
async def get_premium_content(request, response):
if not request.user.has_premium:
raise PaymentRequiredException("Upgrade to premium to access this content")
return response.json({"message": "Premium content available"})
# If your custom exception handler raises an error, sillo will return a 500 error. Always handle exceptions in your handlers gracefully.

sillo provides a way to register custom exception handlers for specific exception types or HTTP status codes. This allows you to define custom error handling behavior for specific errors.

from sillo.exceptions import HTTPException
async def handle_payment_required_exception(request, response, exception):
return response.json({"error": "Payment required"}, status=402)
app.add_exception_handler(PaymentRequiredException, handle_payment_required_exception)
@app.get("/premium-content")
async def get_premium_content(request, response):
if not request.user.has_premium:
raise PaymentRequiredException("Upgrade to premium to access this content")
return response.json({"message": "Premium content available"})

Handle exceptions by status code:

from sillo.exceptions import HTTPException
async def handle_payment_required_exception(request, response, exception):
return response.json({"error": "Payment required"}, status=402)
app.add_exception_handler(402, handle_payment_required_exception)
@app.get("/premium-content")
async def get_premium_content(request, response):
if not request.user.has_premium:
raise PaymentRequiredException("Upgrade to premium to access this content")
return response.json({"message": "Premium content available"})

In the provided example, we demonstrate how to create a custom exception handler for handling specific exceptions in a sillo application. We define a custom exception handler handle_payment_required_exception, which returns a JSON response with an error message and a status code when a PaymentRequiredException is raised. This handler is registered with the application using app.add_exception_handler(). This approach allows for granular control over error responses, improving the user experience by providing clear feedback for specific scenarios, such as when a user tries to access premium content without a subscription.

sillo provides a way to register a custom server error handler using the server_error_handler parameter from silloApp. This allows you to define custom error handling behavior for server errors.

from sillo import silloApp
from sillo.exceptions import HTTPException
async def server_error_handler(request, response, exception):
return response.json({"error": "Internal Server Error"}, status=500)
app = silloApp(server_error_handler=server_error_handler)

Enable debug mode for detailed error responses:

from sillo import silloApp
app = silloApp(debug=True)

sillo allows you to control how 404 Not Found errors are handled and presented to users. This is useful for providing a more user-friendly experience, matching your API or website style, or giving developers more debugging information during development.

A 404 Not Found error is a standard HTTP response code. It means the client (browser, API consumer, etc.) tried to access a URL or resource that doesn’t exist on your server. This could be a mistyped URL, a deleted resource, or a route that was never defined.

  • User Experience: Show a branded or helpful error page/message.
  • API Consistency: Ensure all errors follow your API’s response format.
  • Debugging: Show more details (like tracebacks) in development, but hide them in production.
  • Security: In production, you may want to hide technical details to avoid leaking information about your app’s structure.
  • If a route isn’t found, sillo raises a NotFoundException.
  • The default handler returns a simple JSON or HTML message, depending on the request and debug mode.

You can add a not_found section to your application config. This controls the format and content of 404 responses. This is not a route or a handler you write yourself, but a set of options that tell sillo how to respond when a 404 happens.

OptionTypeWhat it DoesWhen to Use It
return_jsonboolIf True, respond with JSON. If False, use HTML or plain text.APIs: True, Websites: False
custom_messagestrThe message shown for 404 errors (when not in debug mode).Branding, user-friendliness
show_tracebackboolIf True and debug is on, include a Python traceback in the response.Development only
use_htmlboolIf True, use an HTML error page (if not returning JSON).Websites, or pretty error pages
from sillo import silloApp
# ... other config options ...
"not_found": {
"return_json": True, # Set to False for HTML/plain text
"custom_message": "Sorry, this page does not exist.",
"show_traceback": False, # Only relevant if debug is True
"use_html": False, # Set to True for HTML error page
}
})
  • If return_json is True, the response will be JSON:
    {"status": 404, "error": "Not Found", "message": "Sorry, this page does not exist."}
  • If use_html is True and return_json is False, an HTML error page is shown.
  • If both are False, a plain text message is returned.
  • If show_traceback is True and debug mode is on, the response (JSON or HTML) will include a Python traceback, which helps you debug why the route wasn’t found.

A. API-First Project You want all errors, including 404s, to be JSON so your frontend or mobile app can handle them consistently.

"not_found": {
"return_json": True,
"custom_message": "Resource not found.",
"show_traceback": False,
"use_html": False,
}
})

B. Marketing Website You want a pretty, branded HTML error page for users who mistype a URL.

"not_found": {
"return_json": False,
"custom_message": "Oops! We couldn't find that page.",
"show_traceback": False,
"use_html": True,
}
})

C. Developer Debugging You want to see tracebacks for 404s while developing, but not in production.

"debug": True,
"not_found": {
"return_json": True,
"custom_message": "Not here!",
"show_traceback": True,
"use_html": False,
}
})
Use Casereturn_jsonuse_htmlshow_tracebackcustom_message
APITrueFalseFalse”Resource not found.”
WebsiteFalseTrueFalse”Sorry, this page does not exist.”
DebuggingTrue/FalseAnyTrueAny

Tip: You can change these settings at runtime by updating your config and calling set_config().

An error response has one job: tell the caller what to do next. Three pieces of information do that.

A status code they can branch on. The distinction that matters most is retryable versus not. 429 and 503 mean “wait and try again”; 400, 403, 404, and 422 mean “do not”. Getting this wrong produces clients that either give up on transient failures or hammer you forever on permanent ones.

A stable machine-readable identifier. Human-readable messages get reworded; a code field does not. A client localising your errors, or branching on a specific failure, needs something that will not change.

{"error": {"code": "insufficient_funds", "message": "Balance too low", "detail": {...}}}

Enough detail to act, and no more. A validation error should name the field. An internal error should name nothing — a stack trace, a SQL fragment, or a file path in a production response is reconnaissance you handed over for free.

The default sillo behaviour returns detail that is useful in development and dangerous in production: exception text, and in some paths a traceback.

Register a catch-all that logs everything and returns nothing:

the handler every production app needs
@app.add_exception_handler(Exception)
async def unhandled(request, response, exc):
request_id = request.headers.get("x-request-id", "-")
logger.exception("unhandled error [%s] on %s", request_id, request.url.path)
return response.json(
{"error": {"code": "internal_error", "request_id": request_id}},
status_code=500,
)

The request_id is the important half. It gives a user something to quote to support, and it gives you a key to find the full traceback in your logs — all the debuggability, none of the disclosure.

An exception logged in the handler, again in a middleware, and again in the catch-all produces three stack traces for one failure and makes error rates meaningless. Log where you handle, and re-raise everywhere else.

Log at the level the situation deserves: a 404 is INFO, a 422 is INFO, a 403 is WARNING if it is unexpected, and only genuine 5xx failures are ERROR. An alert threshold built on a log level that includes client mistakes will never be actionable.

Error paths are the least-tested code in most applications and the most likely to be wrong when they run, because they run rarely and under stress.

Three tests worth writing per application, not per endpoint. That an unhandled exception produces your generic 500 body and not a traceback. That a validation failure produces the documented 422 shape. And that a 404 for a missing resource is distinguishable from a 404 for a missing route, since clients often need to tell them apart.

Assert on the body shape, not just the status. A 500 that returns a stack trace still has status 500.

An operation that never returns is an error you have not classified. Put a timeout on every network call and decide what its expiry means: a 504 if you were proxying, a 503 if the dependency is optional and you can degrade, a 500 if it means your own logic is stuck.