blog
Ottorino Bruni  

How to Use ProblemDetails in ASP.NET Core APIs for Better Error Handling

Introduction

Every day, when building or maintaining our ASP.NET Core APIs, we often deal with returning basic responses like 200 OK or 404 Not Found. It works the client gets a response, the server does its job, and we move on.

But here’s the thing: we usually don’t put much effort into improving the way our API communicates errors.

And that’s a missed opportunity.

We assume that whoever is consuming our API knows how it works, what went wrong, and what to do next. But that’s rarely true especially when you’re working in a team, exposing your API to external developers, or building a frontend that needs to react smartly to problems.

In this article, I’ll show you how to make your ASP.NET Core API a better citizen by using ProblemDetails, a standardized way to return structured and meaningful error messages.
We’ll start with basic examples, and step by step, we’ll improve them into something more helpful, predictable, and user-friendly.

Let’s dive in.

The Default ASP.NET Core Behavior

Let’s take a look at how a basic ASP.NET Core API usually handles errors.

In a typical controller:

[HttpGet("users/{id}")]
public IActionResult GetUser(int id)
{
    var user = _db.FindUser(id);
    if (user == null)
    {
        return NotFound(); // returns 404 with no details
    }

    return Ok(user);
}

Or using minimal APIs:

app.MapGet("/users/{id}", (int id) =>
{
    var user = db.FindUser(id);
    return user is not null ? Results.Ok(user) : Results.NotFound();
});

These are perfectly valid and they work.

But here’s the problem:
The NotFound() method returns a very simple 404 response with no additional information.

{
  "status": 404,
  "title": "Not Found"
}

It doesn’t tell you what was not found, why, or what to do next.

Now imagine being a frontend developer calling this API it’s not exactly helpful, right?

So How Do We Improve This? To make our API responses more useful, predictable, and developer-friendly, we can start using a standard format called ProblemDetails. Let’s see how it works in the next section.

Introducing ProblemDetails – A Standard for API Errors

To improve the way we return errors from our ASP.NET Core APIs, we can follow a modern standard defined in RFC 9457.
This specification describes a simple and consistent format for representing error conditions in HTTP APIs.

Instead of sending plain messages like "Not Found" or "Bad Request", we can return a structured response that both humans and machines can understand.

This format is called Problem Details.

Here’s how a typical ProblemDetails response looks:

{
  "type": "https://example.com/problems/user-not-found",
  "title": "User not found",
  "status": 404,
  "detail": "No user was found with the given ID.",
  "instance": "/users/123"
}

et’s break down each field:

  • type – A URI that identifies the type of problem. It can point to documentation or be used as a unique identifier.
  • title – A short, human-readable summary of the problem.
  • status – The HTTP status code (e.g. 400, 404, 500).
  • detail – A more detailed explanation about this specific error.
  • instance – The specific path or request that caused the problem.

This structure gives your API consumers much more context especially useful in large systems or when integrating with third-party developers.

And the best part? ASP.NET Core already includes built-in support for this format. You just have to know how to use it.

Let’s start with a basic example.

Example: Using ProblemDetails in an ASP.NET Core Minimal API

Disclaimer: This example is purely for educational purposes. There are better ways to write code and applications that can optimize this example. Use this as a starting point for learning, but always strive to follow best practices and improve your implementation.

Prerequisites

Before starting, make sure you have the following installed:

  • .NET SDK: Download and install the .NET SDK if you haven’t already.
  • Visual Studio Code (VSCode): Install Visual Studio Code for a lightweight code editor.
  • C# Extension for VSCode: Install the C# extension for VSCode to enable C# support.

Step 1 – Create the Minimal API

In your terminal:

dotnet new webapi -minimal -n ProblemDetailsApi
cd ProblemDetailsApi

You’ll now have a Program.cs with a basic setup.

Step 2 – Configure the API for ProblemDetails Support

Before we add any endpoints, let’s make sure our API is set up to return error responses using the ProblemDetails format.

Open Program.cs and make sure to include the following:

// Program.cs

var builder = WebApplication.CreateBuilder(args);

// 1. Register ProblemDetails services
builder.Services.AddProblemDetails();

var app = builder.Build();

// 2. Enable global exception handling
app.UseExceptionHandler();

// 3. Convert empty error responses (like 404 or 403) to ProblemDetails
app.UseStatusCodePages();

Here’s what each line does:

LineWhy it matters
AddProblemDetails()Adds support for generating error responses in the RFC 9457 format (application/problem+json).
UseExceptionHandler()Catches unhandled exceptions globally and formats them as ProblemDetails.
UseStatusCodePages()Ensures that even empty 4xx/5xx responses (like 404) are returned as structured ProblemDetails.

Once these are in place, you’re ready to start building endpoints that either:

  • return ProblemDetails manually,
  • or throw exceptions that are caught and formatted automatically.

Step 3 – Simulate an Error Response with ProblemDetails

Replace the default MapGet with this logic:

app.MapGet("/users/{id}", (int id) =>
{
    var user = FakeDatabase.FindUser(id);

    if (user is null)
    {
        return Results.Problem(
            title: "User not found",
            statusCode: 404,
            detail: $"User with ID {id} does not exist.",
            type: "https://example.com/problems/user-not-found",
            instance: $"/users/{id}"
        );
    }

    return Results.Ok(user);
});

Let’s create the fake data source:

public static class FakeDatabase
{
    private static readonly Dictionary<int, string> Users = new()
    {
        [1] = "Alice",
        [2] = "Otto"
    };

    public static string? FindUser(int id) => Users.TryGetValue(id, out var name) ? name : null;
}

Now calling /users/1 returns 200 OK.
But calling /users/99 returns:

{
  "title": "User not found",
  "status": 404,
  "detail": "User with ID 99 does not exist.",
  "type": "https://example.com/problems/user-not-found",
  "instance": "/users/99"
}

Step 4 – Add Global Exception Handling

Let’s now see how to handle unexpected errors automatically without having to manually return Results.Problem() in each endpoint.

We’ll simulate an exception and use UseExceptionHandler to return a proper ProblemDetails response.

Update Program.cs

First, register the middleware:

app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;

        var problem = new ProblemDetails
        {
            Title = "An unexpected error occurred",
            Status = 500,
            Detail = exception?.Message,
            Type = "https://example.com/problems/internal-error",
            Instance = context.Request.Path
        };

        problem.Extensions["traceId"] = context.TraceIdentifier;

        context.Response.StatusCode = 500;
        context.Response.ContentType = "application/problem+json";
        await context.Response.WriteAsJsonAsync(problem);
    });
});
How to Use ProblemDetails in ASP.NET Core APIs – Program.cs

Create a new endpoint that throws

app.MapGet("/crash", () =>
{
    throw new InvalidOperationException("This was not supposed to happen!");
});

Requesting /crash will return:

{
  "title": "An unexpected error occurred",
  "status": 500,
  "detail": "This was not supposed to happen!",
  "type": "https://example.com/problems/internal-error",
  "instance": "/crash",
  "traceId": "00-4cbb3a4e3a19ac19..."
}

This is powerful because it works across the app, and you don’t have to repeat error handling logic in every endpoint.

How to Use ProblemDetails in ASP.NET Core APIs – Users Api

Full Example – Minimal API with ProblemDetails Support

using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

// Enable ProblemDetails.
builder.Services.AddProblemDetails();

var app = builder.Build();

app.UseExceptionHandler(appError =>
{
    appError.Run(async context =>
    {
        var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;

        var problem = new ProblemDetails
        {
            Title = "An unexpected error occurred",
            Status = 500,
            Detail = exception?.Message,
            Instance = context.Request.Path,
            Type = "https://example.com/problems/internal-server-error"
        };

        problem.Extensions["traceId"] = context.TraceIdentifier;

        context.Response.StatusCode = 500;
        context.Response.ContentType = "application/problem+json";
        await context.Response.WriteAsJsonAsync(problem);
    });
});

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}

app.UseHttpsRedirection();

// Manual ProblemDetails
app.MapGet("/users/{id}", (int id) =>
{
    var user = FakeDatabase.FindUser(id);

    if (user is null)
    {
        return Results.Problem(
            title: "User not found",
            statusCode: 404,
            detail: $"User with ID {id} does not exist.",
            type: "https://example.com/problems/user-not-found",
            instance: $"/users/{id}"
        );
    };
    
    return Results.Ok(user);
});

// Simulate unhandled exception
app.MapGet("/crash", () =>
{ 
    throw new InvalidOperationException("Something broke internally!");
});

app.Run();

// Fake DB
public static class FakeDatabase
{
    private static readonly Dictionary<int, string> Users = new()
    {
        [1] = "Alice",
        [2] = "Otto"
    };

    public static string? FindUser(int id) => Users.TryGetValue(id, out var name) ? name : null;
}

Test Your ProblemDetails Endpoints with an .http File in VS Code

If you’re using Visual Studio Code, you can quickly test all your API endpoints using a simple .http file.
It’s a great alternative to Postman or curl, and keeps everything close to your code.

Here’s an example:

@ProblemDetailsApi_HostAddress = http://localhost:5014

GET {{ProblemDetailsApi_HostAddress}}/users/2
Accept: application/json
###

GET {{ProblemDetailsApi_HostAddress}}/users/99
Accept: application/json
###

GET {{ProblemDetailsApi_HostAddress}}/crash
Accept: application/json
###
How to Use ProblemDetails in ASP.NET Core APIs – Http File

Make sure you have the REST Client extension installed in VS Code.
Then just click “Send Request” above any line to test that endpoint and see the ProblemDetails response.

How to Use ProblemDetails in ASP.NET Core APIs – Run ProblemDetails

Conclusion

Handling errors in a clean and consistent way is one of the most overlooked aspects of API design.
But as we’ve seen in this article, it’s also one of the easiest things to improve especially with the built-in support that ASP.NET Core gives us for the Problem Details RFC (RFC 9457).

By adopting this standard, you provide:

  • A structured format that is easy to parse and consume
  • Clear error messages that help developers understand what went wrong
  • The ability to add custom metadata, like traceId, type, or error codes
  • A much better experience for both humans and machines

This becomes especially important when your API:

  • is consumed by frontend developers or mobile clients
  • is shared across multiple teams
  • is public or documented for external use

With just a few lines of setup (AddProblemDetails, UseExceptionHandler, and UseStatusCodePages), you turn confusing error messages into something predictable, helpful, and standardized.

So next time you’re building or refactoring an ASP.NET Core API, don’t just stop at return NotFound() or throw.
Make your API speak clearly use ProblemDetails.

If you think your friends or network would find this article useful, please consider sharing it with them. Your support is greatly appreciated.

Thanks for reading!

Discover CodeSwissKnife Bar, your all-in-one, offline Developer Tools from Your Menu Bar

Leave A Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.