Building NeuroStash - V

Farhan KhojaFarhan Khoja
5 min read

Custom Exception Handling: From Chaos to Clean API Responses

In Part IV, I built a bulletproof document management system with state machines and atomic operations. But there was still one glaring problem: terrible error messages.

When users send malformed requests to your API, FastAPI's default error responses look like this:

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "files"],
      "msg": "Field required",
      "input": {"wrong_field": "value"},
      "url": "https://errors.pydantic.dev/2.4/v/missing"
    },
    {
      "type": "string_too_short",
      "loc": ["body", "name"],
      "msg": "String should have at least 1 characters",
      "input": "",
      "url": "https://errors.pydantic.dev/2.4/v/string_too_short"
    }
  ]
}

This is developer-hostile. Frontend developers hate parsing nested arrays. Mobile apps crash trying to display this. Your API docs look unprofessional.

I needed something better. Something that makes sense.

The Problem: FastAPI's Default Validation Errors

FastAPI uses Pydantic for request validation, which is fantastic for type safety but terrible for API ergonomics. The default RequestValidationError responses are:

  • Inconsistent structure: Arrays instead of objects

  • Too verbose: Includes internal URLs and type information

  • Hard to parse: Nested location arrays like ["body", "files", 0, "name"]

  • Not user-friendly: Technical jargon instead of clear messages

When building NeuroStash's document upload system, I realized users would constantly hit validation errors:

  • Missing required fields in upload requests

  • Invalid file formats

  • Empty knowledge base names

  • Malformed JWT tokens

Each error needed to be clear, consistent, and actionable.

The Solution: Custom Exception Handler

I designed a custom exception handler that transforms FastAPI's validation chaos into clean, structured responses:

# app/core/exceptions.py
from fastapi import Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse

async def request_validation_exception_handler(
    request: Request, exc: RequestValidationError
):
    formatted_errors = {}

    for error in exc.errors():
        # Convert location array to dot notation
        field_name = ".".join(map(str, error["loc"]))
        message = error["msg"]
        formatted_errors[field_name] = message

    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={
            "detail": {
                "message": "Request body validation failed. Please check the errors",
                "errors": formatted_errors,
            }
        },
    )

What This Handler Does

  1. Flattens nested errors: Converts ["body", "files", 0] to "body.files.0"

  2. Creates key-value pairs: Maps field names directly to error messages

  3. Provides consistent structure: Always returns the same response format

  4. Maintains helpful context: Includes both summary message and specific errors

Before vs After: The Transformation

Before (FastAPI default):

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "files"],
      "msg": "Field required",
      "input": {"name": "My KB"},
      "url": "https://errors.pydantic.dev/2.4/v/missing"
    },
    {
      "type": "string_too_short", 
      "loc": ["body", "name"],
      "msg": "String should have at least 1 characters",
      "input": "",
      "url": "https://errors.pydantic.dev/2.4/v/string_too_short"
    }
  ]
}

After (Custom handler):

{
  "detail": {
    "message": "Request body validation failed. Please check the errors",
    "errors": {
      "body.files": "Field required",
      "body.name": "String should have at least 1 characters"
    }
  }
}

The transformation is dramatic. Frontend developers can now:

  • Access errors directly: response.detail.errors["body.files"]

  • Display user-friendly messages without parsing

  • Build consistent error handling across all endpoints

Integration with NeuroStash

I registered the custom handler in the main application:

# main.py
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from app.core.exceptions import request_validation_exception_handler

app = FastAPI(title=settings.PROJECT_NAME)

# Register the custom exception handler
app.add_exception_handler(
    RequestValidationError, 
    request_validation_exception_handler
)

This single line transforms every validation error across the entire application.

Real-World Impact

Here's how this improved NeuroStash's API experience:

Document Upload Errors

When users forget required fields in document uploads:

# POST /documents/upload
{
  "wrong_field": ["file1.pdf", "file2.docx"]  # Missing 'files' field
}

Response:

{
  "detail": {
    "message": "Request body validation failed. Please check the errors",
    "errors": {
      "body.files": "Field required"
    }
  }
}

Knowledge Base Creation Errors

When knowledge base names are empty:

# POST /kb/create  
{
  "name": ""  # Too short
}

Response:

{
  "detail": {
    "message": "Request body validation failed. Please check the errors", 
    "errors": {
      "body.name": "String should have at least 1 characters"
    }
  }
}

Complex Nested Validation

For complex objects with nested validation errors:

# POST /documents/finalize
{
  "successful": "not-an-array",  # Wrong type
  "failed": [1, 2, "not-a-number"]  # Mixed types
}

Response:

{
  "detail": {
    "message": "Request body validation failed. Please check the errors",
    "errors": {
      "body.successful": "Input should be a valid list",
      "body.failed.2": "Input should be a valid integer"
    }
  }
}

Advanced Pattern: Field-Specific Improvements

The dot notation makes it easy to handle complex validation scenarios:

def get_user_friendly_message(field_name: str, original_message: str) -> str:
    """Convert technical validation messages to user-friendly ones"""

    field_mappings = {
        "body.files": "Please provide a list of files to upload",
        "body.name": "Knowledge base name cannot be empty", 
        "body.successful": "Successful uploads must be a list of document IDs",
        "body.failed": "Failed uploads must be a list of document IDs"
    }

    return field_mappings.get(field_name, original_message)

Error Handling Strategy

This custom handler is part of a broader error handling strategy in NeuroStash:

  1. Validation Errors (422): Handled by custom exception handler

  2. Business Logic Errors (409, 404): Custom exceptions with specific messages

  3. Authentication Errors (401, 403): JWT middleware with clear responses

  4. Server Errors (500): Logged with correlation IDs, generic user message

Each type of error gets appropriate treatment while maintaining consistent response structure.

Best Practices Learned

1. Consistent Response Structure

Every error response follows the same pattern:

{
  "detail": {
    "message": "Human-readable summary",
    "errors": { /* field-specific errors */ }
  }
}

2. Frontend-Friendly Field Names

Dot notation (body.files.0.name) is easy to parse and display in UI forms.

3. Preserve HTTP Status Codes

Keep FastAPI's default status codes (422 for validation) so HTTP clients behave correctly.

4. Don't Leak Internal Details

Remove Pydantic URLs and type information that users don't need.

5. Make Errors Actionable

Each error message should tell users what to fix, not just what's wrong.

Key Takeaways

  • Default FastAPI validation errors are developer-hostile - fix them early

  • Consistent error structure makes frontend integration painless

  • Dot notation field names are easy to parse and display

  • Custom exception handlers transform your entire API with one registration

  • Think about your API consumers - clean errors improve developer experience

Building production APIs isn't just about functionality - it's about creating interfaces that developers actually want to use. Clean error handling is a cornerstone of good API design.


Want to see the full implementation? Check out the NeuroStash repository and follow along as we build something amazing! ๐Ÿš€

Connect with me: @fkhoja098

0
Subscribe to my newsletter

Read articles from Farhan Khoja directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Farhan Khoja
Farhan Khoja