How to Return 400 Instead of 500 With REST API HTTP Status Codes
A Flask handler that reads data["email"] directly raises a KeyError on a request with no email, which Flask turns into a 500 Internal Server Error - the client gets a scary crash for its own mistake. The fix is to validate before you use the field: if email is missing, return 400 with {"error": "email is required"}. A missing field is a client error (4xx), never a server fault (5xx).
Why a missing field should be a 400, not a 500
REST API HTTP status codes split cleanly by their first digit. 4xx means
the client sent something wrong; 5xx means the server broke. A request
that omits a required field is squarely a client error - it belongs in the
4xx range, specifically 400 Bad Request.
The bug is that unvalidated input turns a client mistake into a server crash. This Flask handler reads the field directly:
@app.post("/api/users")
def create_user():
data = request.get_json(silent=True) or {}
# BUG: data["email"] raises KeyError when the field is missing,
# which Flask turns into a 500 Internal Server Error.
email = data["email"]
user_id = next(_next_id)
USERS[user_id] = {"id": user_id, "email": email}
return jsonify(USERS[user_id]), 201
Send a body with no email and data["email"] throws KeyError. Flask
catches the unhandled exception and returns 500. The client learns
nothing about what it did wrong, dev environments leak a stack trace, and
in production a 500 pages your on-call for what is actually a routine bad
request.
Validate before you use the field
Check for the field first and return 400 with a message before touching
it. This is the fix from the project:
@app.post("/api/users")
def create_user():
data = request.get_json(silent=True) or {}
if not data.get("email"):
return jsonify(error="email is required"), 400
email = data["email"]
user_id = next(_next_id)
USERS[user_id] = {"id": user_id, "email": email}
return jsonify(USERS[user_id]), 201
Two details matter. data.get("email") returns None instead of raising
when the key is absent, so the check itself never crashes. And the tuple
(..., 400) sets the status code - without that second value Flask would
default to 200 OK, which is just as wrong in the opposite direction.
The valid path is unchanged: a request with an email still creates the
user and returns 201 Created.
Verify both paths with curl
Test the client-error path and the success path explicitly, printing the status code:
pkill -f "python3 app.py"; python3 app.py > /tmp/flask.log 2>&1 &
# Missing email -> 400 (was 500)
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8000/api/users \
-H 'Content-Type: application/json' -d '{}'
# Valid request -> 201
curl -s -o /dev/null -w '%{http_code}\n' -X POST localhost:8000/api/users \
-H 'Content-Type: application/json' -d '{"email":"a@b.com"}'
The first call should print 400, the second 201. On the frontend, a
React form reading res.status and body.error now shows a precise
message ("400: email is required") instead of a generic failure.
The rule that scales
Validate at the edge, before any handler logic touches the input. Real
codebases formalize this with schema validation - Pydantic in FastAPI, Zod
in TypeScript, Marshmallow in Flask - which auto-returns 400 or 422
with per-field errors so handlers never see unvalidated data. The
principle is the same at any scale: a client's bad input is a 4xx, and
only an actual server fault earns a 5xx.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Distinguishing 4xx client errors from 5xx server errors in a REST API
- Guarding a required field with data.get() and returning 400 before use
- Verifying both the error and success paths with curl -w '%{http_code}'
FAQ
What HTTP status code should a REST API return for a missing required field?
400 Bad Request is the correct code for a missing or invalid required field. It is a client error - the caller sent something wrong - so it belongs in the 4xx range. Some APIs use 422 Unprocessable Entity for semantic validation errors, but 400 is the standard default.
Why does my Flask API return 500 instead of 400 on bad input?
A 500 means an unhandled exception reached Flask. Reading a missing key with data["email"] raises KeyError, which Flask turns into a 500 Internal Server Error. Validate the field first with data.get("email") and return a 400 explicitly, so the client mistake never becomes a server crash.
What is the difference between a 400 and a 500 status code?
A 400 Bad Request means the client sent something wrong - a missing field, invalid JSON, or a bad value. A 500 Internal Server Error means the server itself failed unexpectedly. The first digit is the signal: 4xx is the caller's fault, 5xx is the server's fault.
How do I return a 400 status code in Flask?
Return a tuple where the second element is the code: return jsonify(error="email is required"), 400. Without the second value Flask defaults to 200, so the status must be explicit in every error branch.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →