Hello! Let's get started on our next lesson.
In our previous session, we made our machine learning API much more robust by adding custom Pydantic validators. You learned how to enforce domain-specific rules on input data, ensuring that your model only receives semantically valid feature sets and gracefully rejects bad data with a 422 Unprocessable Entity error.
But what happens if the input data is perfectly valid, yet an error occurs during the prediction process itself? This could be due to a numerical instability in the model for a rare combination of inputs, a bug in your preprocessing logic, or other unexpected issues. If left unhandled, these exceptions would crash the request and return a generic server error.
Today, we'll address this final layer of error handling within our endpoint. Our learning outcome is to implement specific error handling for exceptions that may occur during model inference. We'll explore two powerful techniques:
- A simple
try...exceptblock using FastAPI's built-inHTTPException. - A more advanced and scalable approach using custom exception classes and global exception handlers.
By the end of this lesson, your API will be able to gracefully manage internal errors, log them for debugging, and return a consistent, user-friendly 500 Internal Server Error response.
1. Layers of Error Handling in an ML API
A production-grade API anticipates failure. As you've seen, our API is developing a multi-layered defense against errors.
Integrating Machine Learning Models with FastAPI for Predictions
This article, 'Integrating Machine Learning Models with FastAPI for Predictions', provides a great summary of the different types of errors a model-serving API should handle. It helps put our current task into context.
Read the short section under the heading 'A robust API needs comprehensive error handling'. It outlines three key error types: input validation, model availability, and processing errors. Our focus today is on the third category: processing errors.
As the article mentions, we've already covered input validation errors (handled by Pydantic). Now, we need to tackle processing errors—the exceptions that can occur inside your endpoint's logic, specifically during the call to model.predict().
2. The Direct Approach: try...except with HTTPException
The most straightforward way to catch errors during inference is to wrap the prediction logic in a try...except block right inside your endpoint. If an exception occurs, you catch it, log it, and then raise FastAPI's HTTPException with a 500 status code.
This immediately prevents the server from crashing and allows you to control the error response sent to the client.
Here's what the implementation looks like in your /predict endpoint:

Let's integrate this into your code. Update your /predict endpoint to include the try...except block.
# In your main.py or relevant router file
from fastapi import FastAPI, HTTPException, Depends
# ... other imports
import logging
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# ... your app initialization and model loading logic ...
@app.post("/predict", response_model=PredictionOutput)
async def predict(input_data: PredictionInput):
"""
Make a prediction using the loaded model.
"""
try:
# Your existing prediction logic
features = [[
input_data.sepal_length,
input_data.sepal_width,
input_data.petal_length,
input_data.petal_width
]]
prediction = ml_model.predict(features)[0]
# You might have logic here to map the prediction index to a class name
# For simplicity, we'll assume the model returns a direct result
return PredictionOutput(
prediction_label=str(prediction), # Adjust based on your PredictionOutput schema
# ... other fields
)
except Exception as e:
logger.error(f"Error during model inference: {e}")
raise HTTPException(
status_code=500,
detail=f"An internal error occurred during model inference: {e}"
)
To test this:
- Temporarily modify your prediction logic to force an error. A simple way is to add
result = 1 / 0inside thetryblock. - Restart your Uvicorn server.
- Send a valid request to the
/predictendpoint via the Swagger UI. - You should receive a
500 Internal Server Errorwith the detail message you defined. Check your terminal, and you'll see the error message you logged withlogger.error().
This approach is effective and perfectly fine for smaller applications. However, for larger projects with multiple endpoints that could fail, repeating this try...except logic everywhere can lead to code duplication.
3. The Professional Approach: Custom Exception Handlers
For more scalable and maintainable applications, FastAPI provides a powerful mechanism called exception handlers. This allows you to define a single, global function responsible for handling a specific type of exception whenever it's raised anywhere in your application.
This process involves three steps:
- Define your own custom exception class.
- Raise this custom exception from your endpoint's
exceptblock. - Register a handler function that tells FastAPI how to convert that exception into a JSON response.
The following video provides an excellent high-level explanation of this pattern.
This Is How You Do PROPER Exception Handling With FastAPI
In his video 'This Is How You Do PROPER Exception Handling With FastAPI', ArjanCodes clearly explains the benefits of creating a custom exception hierarchy and registering handlers for them.
Watch from 00:42 to 05:07. Focus on these key ideas: 00:42: Creating a custom base exception class that inherits from Exception. 02:43: Using app.add_exception_handler to link a specific exception type to a handler function. 04:02: Raising the custom exception in an endpoint and seeing the formatted JSON response.
Now, let's apply this professional pattern to our API.
Step 1: Create a Custom Exception
First, create a new file named exceptions.py in your project's source directory (app/ or src/). This file will house our application-specific exceptions.
# In exceptions.py
class ModelInferenceError(Exception):
"""Custom exception for errors during model inference."""
def __init__(self, detail: str):
self.detail = detail
This creates a new exception type that is specific to our application's domain.
Step 2: Register the Exception Handler
Next, in your main.py, you'll define the handler function and register it with your FastAPI app instance. The handler will catch our ModelInferenceError and return a structured JSONResponse.
# In main.py
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from .exceptions import ModelInferenceError # Import your custom exception
import logging
# ...
# app = FastAPI(...)
@app.exception_handler(ModelInferenceError)
async def model_inference_exception_handler(request: Request, exc: ModelInferenceError):
"""
Global handler for ModelInferenceError.
Returns a 500 status code with a custom JSON response.
"""
logger.error(f"ModelInferenceError occurred: {exc.detail}")
return JSONResponse(
status_code=500,
content={
"message": "An unexpected error occurred during model inference.",
"error_detail": exc.detail,
"error_code": "MODEL_INFERENCE_FAILED" # A useful code for clients
},
)
Using the @app.exception_handler(...) decorator is a clean way to register the handler. Now, any time ModelInferenceError is raised, this function will take over to generate the response.
Step 3: Refactor the Endpoint to Raise the Custom Exception
Finally, modify your /predict endpoint. The try...except block now becomes much simpler. Its only job is to catch a generic Exception and re-raise it as our specific ModelInferenceError.
# In main.py, within the /predict endpoint
# ...
from .exceptions import ModelInferenceError
# ...
@app.post("/predict", response_model=PredictionOutput)
async def predict(input_data: PredictionInput):
"""
Make a prediction using the loaded model.
"""
try:
# Your existing prediction logic...
features = [[
input_data.sepal_length,
input_data.sepal_width,
input_data.petal_length,
input_data.petal_width
]]
prediction = ml_model.predict(features)[0]
# To test, uncomment the next line:
# raise ValueError("Simulating a model failure")
return PredictionOutput(
prediction_label=str(prediction),
# ... other fields
)
except Exception as e:
# Catch any exception and raise our custom, structured error
raise ModelInferenceError(detail=str(e))
Now, test this again by forcing an error. You will see your new, structured JSON response, and the logic for creating it is neatly centralized in the handler, not inside the endpoint. This makes your endpoint code cleaner and your error handling consistent across the entire application.
Conclusion
Excellent work! You've now implemented a critical feature for any production-ready API: robust handling of internal processing errors.
Key Takeaways:
- Model inference can fail for reasons beyond invalid input, requiring a layer of error handling around the
model.predict()call. - The
try...except...raise HTTPExceptionpattern is a simple and direct way to handle endpoint-specific errors. - For more scalable and maintainable code, defining custom exception classes and registering global exception handlers with
@app.exception_handleris the best practice. - Global handlers centralize error-response logic, promote consistent error formats, and keep endpoint code focused on the primary business logic.
In our next lesson, we'll move into Module 9, "Advanced I/O: Files and Background Tasks." We'll start by tackling a very common use case in AI: modifying your ML endpoint to accept an image upload using UploadFile for inference.