backend/handling-errors/SKILL.md
Implement centralized error handling in Flask with custom exceptions, global handlers, and structured error responses.
npx skillsauth add 7a336e6e/skills Handling ErrorsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Build a centralized error handling system for a Flask API that uses custom exception classes, registers global error handlers, returns consistent JSON error responses, and logs errors with full request context.
Define a base API exception and specific subclasses. Each exception carries a status code, an error code string, and an optional details payload.
# app/exceptions.py
class APIError(Exception):
"""Base exception for all API errors."""
status_code = 500
error_code = "INTERNAL_ERROR"
message = "An unexpected error occurred."
def __init__(self, message=None, details=None):
super().__init__(message or self.message)
self.message = message or self.message
self.details = details
def to_dict(self):
payload = {
"code": self.error_code,
"message": self.message,
}
if self.details:
payload["details"] = self.details
return payload
class NotFoundError(APIError):
status_code = 404
error_code = "NOT_FOUND"
message = "The requested resource was not found."
class ValidationError(APIError):
status_code = 422
error_code = "VALIDATION_ERROR"
message = "The request data is invalid."
class AuthError(APIError):
status_code = 401
error_code = "AUTH_ERROR"
message = "Authentication is required."
class ForbiddenError(APIError):
status_code = 403
error_code = "FORBIDDEN"
message = "You do not have permission to perform this action."
class ConflictError(APIError):
status_code = 409
error_code = "CONFLICT"
message = "The request conflicts with the current state of the resource."
Register handlers in create_app() so every unhandled exception gets a consistent JSON response.
# app/error_handlers.py
import logging
from flask import jsonify, request, g
from app.exceptions import APIError
logger = logging.getLogger(__name__)
def register_error_handlers(app):
"""Register global error handlers on the Flask app."""
@app.errorhandler(APIError)
def handle_api_error(error):
response = jsonify({"error": error.to_dict()})
response.status_code = error.status_code
logger.warning(
"api_error",
extra={
"error_code": error.error_code,
"status_code": error.status_code,
"message": error.message,
"path": request.path,
"request_id": getattr(g, "request_id", None),
},
)
return response
@app.errorhandler(404)
def handle_not_found(error):
return jsonify({"error": {"code": "NOT_FOUND", "message": "Resource not found."}}), 404
@app.errorhandler(405)
def handle_method_not_allowed(error):
return jsonify({"error": {"code": "METHOD_NOT_ALLOWED", "message": "Method not allowed."}}), 405
@app.errorhandler(500)
def handle_internal_error(error):
logger.exception(
"unhandled_exception",
extra={
"path": request.path,
"method": request.method,
"request_id": getattr(g, "request_id", None),
},
)
return jsonify({"error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred."}}), 500
All error responses follow this structure:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request data is invalid.",
"details": {
"email": ["Not a valid email address."],
"name": ["Missing data for required field."]
}
}
}
code is a machine-readable string that clients can switch on.message is a human-readable summary.details is optional and holds field-level or context-specific information.Raise custom exceptions anywhere in the application. The global handler catches them.
from app.exceptions import NotFoundError, ValidationError
def get_user_or_404(user_id):
user = User.query.get(user_id)
if user is None:
raise NotFoundError(f"User with id {user_id} not found.")
return user
def create_user(data):
if User.query.filter_by(email=data["email"]).first():
raise ConflictError("A user with this email already exists.")
user = User(**data)
db.session.add(user)
db.session.commit()
return user
Always include the request ID, user identity (if available), and endpoint in error logs. For 500-level errors, use logger.exception() to capture the stack trace in logs without exposing it to the client.
logger.exception(
"unhandled_exception",
extra={
"request_id": getattr(g, "request_id", None),
"user_id": getattr(g, "current_user_id", None),
"endpoint": request.endpoint,
"method": request.method,
"path": request.path,
},
)
from app.error_handlers import register_error_handlers
def create_app(config_name=None):
app = Flask(__name__)
app.config.from_object(config_by_name[config_name or "development"])
register_error_handlers(app)
# ... extensions, blueprints, middleware
return app
APIError base.APIError, 404, 405, and 500.code, message, and optional details.logger.exception() to capture stack traces.NotFoundError, ValidationError) instead of generic exceptions.Exception in route handlers; let errors propagate to the global handler.Generate app/exceptions.py and app/error_handlers.py as separate files. Show the updated create_app() with register_error_handlers(app) called.
development
Implement features using the Red-Green-Refactor cycle to ensure testability and correctness from the start.
data-ai
Manage the `tasks.md` ledger with strict locking and collision avoidance protocols to allow multiple agents to work in parallel safely.
development
The git-workflow skill defines branching conventions, commit message formats, and pull request standards that all agents must follow for consistent version control.
development
The environment-config skill standardizes how agents manage environment variables, secrets, and application configuration across local development and deployed environments.