Skip to content

Error Handling

A great API isn’t just defined by how it works when things go right, but also by how it behaves when things go wrong. Clear, consistent, and secure error handling is a cornerstone of a professional API.

SliceFlow provides a multi-layered, robust strategy for error handling that ensures developers consuming your API get helpful feedback, while your application remains secure and predictable, even in the face of unexpected failures.

The Art of Failing Gracefully

In any application, errors are a fact of life. A user might provide invalid data, a requested resource might not exist, or an unexpected bug could cause a catastrophic failure. A well-designed API handles all of these scenarios gracefully. The goal is twofold:

  1. For the API Consumer: Provide clear, structured, and helpful error messages for predictable issues (like bad input) so they can correct their request.
  2. For the API Provider: Safely catch all unexpected exceptions, log them in detail for debugging, and present a generic, secure response to the client without leaking sensitive implementation details.

SliceFlow achieves this through a combination of standardized error formats and a global exception handling safety net.

ProblemDetails: The Universal Language of API Errors

To ensure consistency, SliceFlow uses ProblemDetails (defined in RFC 7807) as the standard format for all client-facing error responses. This means that whether the error is a 400 Bad Request, a 404 Not Found, or a 500 Internal Server Error, the client can always expect a JSON response with a predictable structure.

This is configured out of the box in Endpoints.cs:

// ...
o.Errors.UseProblemDetails(x =>
{
x.IndicateErrorCode = true;
x.IndicateErrorSeverity = true;
});
// ...

Layer 1: Handling Predictable Errors (4xx Status Codes)

These are the errors you anticipate. They are not bugs; they are the result of the client sending a request that the server cannot process as-is.

Validation Failures (400 Bad Request)

As detailed in the Validation documentation, SliceFlow uses a powerful validation system. If a request fails validation (either automatically via a Validator class or manually via ThrowIfAnyErrors()), the framework immediately stops processing and sends back a detailed ProblemDetails response.

{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Permissions": [
"'Permissions' must not be empty."
]
}
}

This response gives the API consumer precise, field-level feedback on what they need to fix.

Resource Not Found (404 Not Found)

If an endpoint queries for a resource that doesn’t exist (e.g., GET /users/{id} where the ID is not in the database), you can easily return a standard 404 response.

if (user is null)
{
return TypedResults.NotFound();
}

This will also be formatted as a standard ProblemDetails response, ensuring consistency for the client.

Layer 2: The Safety Net — The Global Exception Handler (500 Status Code)

This is the most critical layer. What happens if a bug in your code causes an unexpected NullReferenceException, or the database connection suddenly drops? These are the errors you didn’t anticipate.

Without a safety net, these exceptions would crash the request and potentially return a raw HTML error page or, even worse, a detailed stack trace to the client. Leaking stack traces is a major security vulnerability, as it gives attackers a roadmap of your application’s internal structure.

SliceFlow solves this with a GlobalExceptionHandler. This is a special class that acts as a final backstop for your entire application. It is registered in Program.cs:

// ...
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();
// ...
app.UseExceptionHandler();
// ...

Here’s how it works:

  1. Catch Everything: If any exception occurs in your application that is not caught by your own code, the GlobalExceptionHandler takes over.
  2. Log the Details: The very first thing it does is log the full, detailed exception with its stack trace. This is critical for you, the developer, to diagnose and fix the bug. This detailed information is stored safely in your logs (e.g., in Loki) and is never sent to the client.
  3. Create a Safe Response: It then crafts a generic, public-facing ProblemDetails object. Notice that it does not include the exception’s message or any other sensitive details.
  4. Send to the Client: Finally, it sends a 500 Internal Server Error response to the client with the safe ProblemDetails body.
// The GlobalExceptionHandler in action
internal sealed class GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken ct)
{
// Step 2: Log the full details for developers
logger.LogError(exception, "Exception occurred: {Message}", exception.Message);
// Step 3: Create a generic, safe response for the client
var problemDetails = new ProblemDetails
{
Detail = "An unexpected error occurred.",
Status = StatusCodes.Status500InternalServerError
};
// Step 4: Send the safe response
httpContext.Response.StatusCode = problemDetails.Status;
await httpContext.Response.WriteAsJsonAsync(problemDetails, ct);
return true;
}
}

This two-layered approach provides the best of both worlds: clear, actionable feedback for predictable client errors, and a robust, secure safety net for unexpected server failures.