from flask import jsonify
from marshmallow import ValidationError as MarshmallowValidationError
from werkzeug.exceptions import HTTPException
class ApiError(Exception):
status = 400
code = "error"
def __init__(self, message, status=None, code=None, details=None):
super().__init__(message)
self.message = message
if status is not None:
self.status = status
if code is not None:
self.code = code
self.details = details or {}
def to_dict(self):
return {
"error": {
"code": self.code,
"message": self.message,
"details": self.details,
}
}
class ValidationError(ApiError):
status = 422
code = "validation_error"
def register_error_handlers(app):
@app.errorhandler(ApiError)
def handle_api_error(err):
return jsonify(err.to_dict()), err.status
@app.errorhandler(MarshmallowValidationError)
def handle_schema_error(err):
payload = {
"error": {
"code": "validation_error",
"message": "Request body failed validation.",
"details": err.messages,
}
}
return jsonify(payload), 422
@app.errorhandler(HTTPException)
def handle_http_error(err):
payload = {
"error": {
"code": err.name.lower().replace(" ", "_"),
"message": err.description,
"details": {},
}
}
return jsonify(payload), err.code
from marshmallow import Schema, fields, validate
class UserSchema(Schema):
email = fields.Email(required=True)
name = fields.Str(
required=True,
validate=validate.Length(min=1, max=120),
)
age = fields.Int(
required=False,
validate=validate.Range(min=0, max=150),
)
user_schema = UserSchema()
from flask import Flask, request, jsonify
from errors import ValidationError, register_error_handlers
from schemas import user_schema
_users = {}
def create_app():
app = Flask(__name__)
register_error_handlers(app)
@app.route("/users", methods=["POST"])
def create_user():
# marshmallow raises ValidationError -> caught by the schema handler
data = user_schema.load(request.get_json(force=True))
if data["email"] in _users:
raise ValidationError(
"A user with this email already exists.",
details={"email": ["must be unique"]},
)
_users[data["email"]] = data
return jsonify(data), 201
@app.route("/users/<email>", methods=["GET"])
def get_user(email):
user = _users.get(email)
if user is None:
raise ValidationError(
"No user found for that email.",
status=404,
code="not_found",
)
return jsonify(user)
return app
if __name__ == "__main__":
create_app().run(debug=True)
This snippet shows how a Flask API turns messy exceptions into a single, predictable JSON envelope so clients never have to parse HTML error pages or guess at status codes. The core idea is to funnel every failure — application-level validation, schema validation, and generic HTTP errors — through one place, giving the whole service a consistent contract.
In errors.py, ApiError is a small base exception carrying a machine-readable code, a human message, an HTTP status, and optional details. ValidationError subclasses it and defaults to a 422 status with a validation_error code, which is the semantically correct response for a well-formed request that fails business rules. The to_dict method defines the wire format once, so every error response shares the same shape.
The register_error_handlers function is where the wiring happens. app.errorhandler(ApiError) catches any custom error raised deep in the call stack and serialises it with jsonify, setting the status from the exception itself. A second handler registered for marshmallow's MarshmallowValidationError adapts a third-party library's error format (err.messages, a dict of field to message lists) into the same envelope, so schema failures look identical to hand-raised ones. The final handler catches Werkzeug's HTTPException base class, which covers 404, 405, and friends, mapping each to the shared format via its code and name attributes. This layering matters: order of specificity is handled by Flask matching the most specific registered class, so ApiError wins over a plain Exception.
In app.py, create_app follows the application-factory pattern and calls register_error_handlers(app) during setup. The POST /users route loads and validates the body with UserSchema().load, and simply lets a raised MarshmallowValidationError propagate — no try/except clutter in the route. It also raises the custom ValidationError for a domain rule (duplicate email) that a schema can't express, showing how both paths converge on the same output.
The trade-off is that all responses now depend on the handlers being registered; a route that returns raw dicts on error would break the contract. The payoff is that clients get one parser, one code field to branch on, and reliable status codes. A common pitfall avoided here is letting HTTPException fall through to a generic 500 handler, which would strip the correct status. This pattern is worth reaching for the moment an API has more than a couple of endpoints or any non-trivial validation.
Related snips
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
class SignupForm
include ActiveModel::Model
include ActiveModel::Attributes
attribute :account_name, :string
attribute :email, :string
Shallow Controller, Deep Params: Form Object Pattern
// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
Promises and async/await patterns for asynchronous JavaScript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Form Validation Example</title>
<style>
HTML forms with validation and accessibility
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
package api
import (
"io"
"net/http"
"os"
Safe multipart uploads using temp files (bounded memory)
Share this code
Here's the card — post it anywhere.