Flask API Fundamentals · Structure and Error Handling

Error Responses That Help

Return consistent JSON errors from Flask with one error shape, HTTPException and custom handlers, and the right status codes.

Create a free account to save this page, take notes and track your progress. Sign in

Errors are part of your API. A client that gets {"error": "validation failed", "fields": {"email": "already registered"}} can show the user exactly what to fix. A client that gets an HTML page saying "500 Internal Server Error" can only give up. This lesson shows how to make every error in a Flask API come back as consistent JSON.

Pick one error shape

Decide on a single shape and use it everywhere, so clients need one parser:

{
  "error": "validation_failed",
  "message": "Some fields are invalid.",
  "fields": {"email": "already registered"}
}
  • error is a stable code that clients can check in an if statement.
  • message is for people and can change wording later.
  • fields appears only for validation errors.

If you'd rather follow a standard, RFC 9457 "Problem Details" defines a similar shape (type, title, status, detail) with the application/problem+json content type.

Turn Flask's HTTP errors into JSON

By default abort(404) and unknown URLs return an HTML page. One handler for HTTPException converts all of them, including 404, 405 and 415, into your JSON shape:

from flask import Flask, jsonify
from werkzeug.exceptions import HTTPException

app = Flask(__name__)


@app.errorhandler(HTTPException)
def http_error(e):
    code = e.name.lower().replace(" ", "_")
    return jsonify(error=code, message=e.description), e.code

Now GET /nope returns {"error": "not_found", ...} with status 404.

Raise your own errors

For errors that come from your business rules, define one exception class and raise it from anywhere in the code. A single handler turns it into a response:

class APIError(Exception):
    def __init__(self, status, error, message, fields=None):
        super().__init__(message)
        self.status, self.error, self.message, self.fields = status, error, message, fields


@app.errorhandler(APIError)
def api_error(e):
    body = {"error": e.error, "message": e.message}
    if e.fields:
        body["fields"] = e.fields
    return jsonify(body), e.status


@app.post("/users")
def create_user():
    raise APIError(409, "email_taken", "That email is already registered.", {"email": "already registered"})

Handlers stay short: they raise and move on, and the response format lives in one place. The same idea in plain Python, so you can run it:

One place that formats every errorTry it yourself
class APIError(Exception):
    def __init__(self, status, error, message, fields=None):
        super().__init__(message)
        self.status, self.error, self.message, self.fields = status, error, message, fields


def to_response(exc):
    if isinstance(exc, APIError):
        body = {"error": exc.error, "message": exc.message}
        if exc.fields:
            body["fields"] = exc.fields
        return exc.status, body
    # Anything unexpected: log it on the server, but never send the details to the client.
    return 500, {"error": "internal_error", "message": "Something went wrong. Please try again."}


errors = [
    APIError(404, "user_not_found", "No user with id 7."),
    APIError(422, "validation_failed", "Some fields are invalid.", {"email": "must contain @"}),
    KeyError("password_hash"),
]
for exc in errors:
    print(to_response(exc))

Output

(404, {'error': 'user_not_found', 'message': 'No user with id 7.'})
(422, {'error': 'validation_failed', 'message': 'Some fields are invalid.', 'fields': {'email': 'must contain @'}})
(500, {'error': 'internal_error', 'message': 'Something went wrong. Please try again.'})

Don't leak details on 500

When something unexpected fails, log the full stack trace on the server and send the client a generic message. Stack traces reveal file paths, library versions and sometimes data:

@app.errorhandler(Exception)
def unexpected_error(e):
    if isinstance(e, HTTPException):
        return http_error(e)
    app.logger.exception("Unhandled error")
    return jsonify(error="internal_error", message="Something went wrong. Please try again."), 500

Choose the right status code

Status Meaning for the client
400 The request can't be read (broken JSON, wrong type of body)
401 Not signed in, or the token is missing or expired
403 Signed in, but not allowed to do this
404 The resource doesn't exist
409 Conflicts with the current state, such as a duplicate email
422 Readable, but the values are invalid
429 Too many requests; add a Retry-After header
500 A bug or failure on the server; the client did nothing wrong

The difference between 401 and 403 matters: 401 tells the client to sign in again, 403 tells it that signing in again won't help.

Common mistakes

  • Returning 200 with {"success": false}. Caches, retries and monitoring all read the status code, so they treat this as success.
  • Different shapes per endpoint. One endpoint returns {"error": ...}, another {"message": ...}, a third a plain string. Clients end up with special cases everywhere.
  • Leaking exception text. str(e) from a database error can include SQL and table names. Log it; don't return it.
  • Using messages as codes. If clients match on message, fixing a typo breaks them. Give them a stable error code instead.

Try it

Add the HTTPException handler to an app, then request a URL that doesn't exist and send a DELETE to a route that only allows GET. Check that both return JSON with 404 and 405.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?