Error Handling and Exception Middleware
Level: Beginner to Intermediate
Error handling teaches how your app should respond when something fails. This is essential before deploying APIs to real users.
- Error handling purpose: Gracefully handle failures, log issues, return helpful response to client
- Expected errors: Validation errors (missing name), business logic errors (student already exists)
- Unexpected exceptions: Null reference, database connection failed, code bug
- Development error response: Detailed stack trace (helps debugging), shows SQL query, line number
- Production error response: Generic "An error occurred" (don't expose internals), log details server-side
app.UseExceptionHandler("/error"): Middleware catches all unhandled exceptions, redirects to error endpoint- Exception filter:
[ApiControllerAttribute]automatically catches exceptions, returns 500 with problem details - try-catch pattern: Catch expected exceptions, throw custom exceptions for validation errors
- Logging exceptions:
_logger.LogError(ex, "Failed to create student")includes exception + message - Custom exception types:
throw new StudentNotFoundException("Student not found")vs generic exception - Error response format: Problem Details (RFC 7807) with type, title, status, detail fields
- School Management errors: Student 404, duplicate roll number 409, database error 500
- Client error codes: 400 (bad request), 401 (unauthorized), 403 (forbidden), 404 (not found)
- Server error codes: 500 (internal error), 503 (service unavailable), 504 (timeout)
- Common mistakes: Returning 200 with error message inside JSON (client thinks success!), exposing stack traces in production
- Common mistakes in production error handling
Why Error Handling Matters
Every real application fails sometimes.
Examples in a school system:
- database connection fails
- student id does not exist
- parent sends invalid payment data
- file upload is too large
- SMS provider is down
- exam result publishing throws an exception
Good error handling does two things:
User sees a safe, understandable response.
Developer gets enough logs to fix the issue.
Expected Errors vs Unexpected Exceptions
Not every failure is an exception.
| Situation | Type | Response |
|---|---|---|
| Student not found | Expected | 404 NotFound |
| Invalid admission form | Expected | 400 BadRequest |
| User not logged in | Expected | 401 Unauthorized |
| Database connection failed | Unexpected | 500 Internal Server Error |
| Null reference bug | Unexpected | 500 Internal Server Error |
Use action results for expected errors.
Use exception handling middleware for unexpected failures.
Development vs Production
Development should show details to help developers.
Production should hide technical details from users.
| Environment | Error Behavior |
|---|---|
| Development | Detailed exception page |
| Production | Generic error response and server-side logs |
Basic Error Pipeline
Important rule:
Do not show developer exception pages in production.
API Error Endpoint
For APIs, the /error endpoint can return JSON.
using Microsoft.AspNetCore.Diagnostics;
app.Map("/error", (HttpContext context, ILogger<Program> logger) =>
{
var feature = context.Features.Get<IExceptionHandlerPathFeature>();
var exception = feature?.Error;
if (exception != null)
{
logger.LogError(
exception,
"Unhandled exception on path {Path}",
feature?.Path);
}
return Results.Problem(
title: "An unexpected error occurred.",
statusCode: StatusCodes.Status500InternalServerError);
});
This logs the real exception but returns a safe response.
ProblemDetails
ProblemDetails is a standard error response format for APIs.
Example response:
{
"type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
"title": "An unexpected error occurred.",
"status": 500
}
It gives API clients a predictable structure.
Expected Error Example
Do not throw exceptions for normal cases like "student not found".
[HttpGet("{id}")]
public IActionResult GetStudent(int id)
{
if (id <= 0)
{
return BadRequest(new { message = "Invalid student id." });
}
var student = _studentService.GetStudent(id);
if (student == null)
{
return NotFound(new { message = "Student not found." });
}
return Ok(student);
}
This is not exception handling. This is normal API response handling.
Try-Catch in Actions
Use try-catch when you can handle the error meaningfully.
[HttpPost("publish-results")]
public IActionResult PublishResults(PublishResultsRequest request)
{
try
{
_resultService.Publish(request.ExamId);
return Ok(new { message = "Results published." });
}
catch (ExamNotFinalizedException)
{
return BadRequest(new { message = "Exam is not finalized yet." });
}
}
If you cannot handle the exception properly, let global middleware handle it.
Logging Exceptions
Always pass the exception object to LogError.
catch (Exception ex)
{
_logger.LogError(
ex,
"Failed to publish results for exam {ExamId}",
request.ExamId);
return StatusCode(500, new
{
message = "Unable to publish results right now."
});
}
This keeps stack trace information in logs.
What Not to Show Users
In production, never expose stack traces, SQL errors, connection strings, file paths, secret keys, or internal class names to users.
Bad response:
SqlException: Login failed for user...
ConnectionString=Server=...
at School.Data.StudentRepository...
Better response:
{
"message": "Unable to process the request right now."
}
Custom Exception Types
Custom exceptions can represent business failures.
public class AdmissionClosedException : Exception
{
public AdmissionClosedException()
: base("Admissions are closed for this class.")
{
}
}
Then handle it:
catch (AdmissionClosedException ex)
{
return BadRequest(new { message = ex.Message });
}
Do not create custom exceptions for every small validation rule.
Middleware vs Filters vs Try-Catch
| Approach | Best For |
|---|---|
| Try-catch | Expected specific failure inside one action |
| Exception filter | Controller-specific exception handling |
| Exception middleware | Global unhandled exception handling |
For most APIs:
Use action results for expected errors.
Use global exception middleware for unexpected errors.
Use logging everywhere important.
Common Mistakes
| Mistake | Better Approach |
|---|---|
| Showing stack traces in production | Return safe generic errors |
| Catching exceptions and ignoring them | Log useful context |
| Throwing exceptions for validation errors | Return BadRequest |
Returning 200 OK for failures | Use correct status codes |
| Logging passwords/tokens | Never log secrets |
| Handling every exception in every action | Use middleware for global failures |
Practice Task
Create error handling for a school API:
- Add
UseDeveloperExceptionPage()only for Development. - Add
UseExceptionHandler("/error")for Production. - Create
/errorendpoint returningResults.Problem. - Log the exception path and message.
- Return
NotFoundfor missing students without throwing.
Quick Recap
| Question | Answer |
|---|---|
| Dev detailed errors? | UseDeveloperExceptionPage() |
| Production global handler? | UseExceptionHandler() |
| Standard API error shape? | ProblemDetails |
| Missing record response? | 404 NotFound |
| Invalid input response? | 400 BadRequest |
Q: How do you handle errors in ASP.NET Core?
Good Answer: "In ASP.NET Core, expected errors such as validation failures or missing records should be returned using proper action results like BadRequest and NotFound. Unexpected exceptions should be handled by global exception middleware using UseExceptionHandler. In development, UseDeveloperExceptionPage can show detailed errors, but in production the app should return safe generic responses and log full exception details with ILogger. APIs commonly use ProblemDetails for standard error responses."
Use ChatGPT, Claude, or Copilot to go deeper on ASP.NET Core Error Handling. Try these prompts:
"Explain expected errors vs unexpected exceptions in ASP.NET Core.""Show me production-safe exception middleware for Web API.""What should never be shown in production error responses?""When should I use try-catch instead of global exception middleware?"
💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.