Lesson 14 of 45 intermediate

Exception Handling and Logging

Catch errors in one place, tell clients little, and tell yourself everything

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

What is the difference between throw; and throw ex; inside a catch block?

Open interactive version (quiz + challenge)

Real-world analogy

Think of a hospital emergency desk. Patients with problems do not wander between wards. They all reach one desk, which gives them a simple ticket and writes the full details in a private chart. The waiting room never sees the chart. Here the desk is the global exception handler, the ticket is ProblemDetails, and the private chart is the log.

What is it?

Exception handling decides what happens when code fails. In ASP.NET Core you register one handler in the pipeline that turns exceptions into consistent HTTP responses, using the ProblemDetails format. Logging records what happened, through ILogger<T>, with levels and named fields. Together they give clients a safe short error and give developers the full story.

Real-world relevance

Without a global handler every controller grows its own try/catch, with different JSON shapes and some forgotten ones that return a raw HTML error page. Teams then wire the trace id into the error body, so a customer can send "my request id is abc" and the developer finds the exact log line. Structured logs also let you ask "show all failures for OrderId 42" without grepping text.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

Which log call keeps OrderId as a searchable field?

An unexpected NullReferenceException reaches the global handler. What should the client receive?

Code example

public class NotFoundException : Exception
{
    public NotFoundException(string message) : base(message) { }
}
public class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;
    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger) => _logger = logger;
    public async ValueTask<bool> TryHandleAsync(
        HttpContext context, Exception exception, CancellationToken ct)
    {
        var (status, title) = exception switch
        {
            NotFoundException => (StatusCodes.Status404NotFound, "Resource not found"),
            _ => (StatusCodes.Status500InternalServerError, "Something went wrong")
        };
        // Template with named fields, and the exception object keeps the stack trace
        _logger.LogError(exception, "Request {TraceId} failed with {Status}",
            context.TraceIdentifier, status);
        context.Response.StatusCode = status;
        await context.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = status,
            Title = title,
            Extensions = { ["traceId"] = context.TraceIdentifier }
        }, ct);
        return true;   // handled, stop here
    }
}

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

Line-by-line walkthrough

  1. 1. NotFoundException is a small custom type. Services throw it, and the handler decides what HTTP code it means.
  2. 2. GlobalExceptionHandler implements IExceptionHandler. The logger arrives through the constructor from DI.
  3. 3. The switch maps NotFoundException to 404. The discard case maps everything else to a generic 500.
  4. 4. LogError gets the exception object plus a template with TraceId and Status, so the stack trace and the fields are both saved.
  5. 5. The handler writes a ProblemDetails body with the status, a short title and the trace id. No stack trace is sent.
  6. 6. Returning true tells ASP.NET Core the error is handled. The three registration lines at the end wire it up.

Spot the bug

public async Task<Order> GetOrderAsync(int id)
{
    try
    {
        return await _repo.FindAsync(id);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Loading order {OrderId} failed", id);
        throw ex;
    }
}
Need a hint?
Compare throw ex; with throw; when you read the stack trace later.
Show answer
throw ex; resets the stack trace to this line, so the real origin inside the repository is lost. Fix: use throw; to rethrow with the original trace. Better still, remove the catch and let the global handler log it once.

Explain like I'm 5

When something breaks, everyone goes to one helper desk. The desk gives you a simple note that says what kind of problem it was. The desk writes the full story in a private notebook for the grown-ups.

Fun fact

A message template like "Order {OrderId}" is not turned into one plain string when you log. The template and the value reach the log provider separately, so tools can group thousands of lines by the same template.

Hands-on challenge

Say it out loud in 30 seconds: "How do you handle exceptions and logging in an ASP.NET Core API?" Then check: did you mention one global handler, ProblemDetails, structured log templates, and not leaking stack traces or secrets?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook