Error Handling
Level: Intermediate
ℹ️ What You'll Learn
- Exception types: Expected (validation, not found) vs Unexpected (null reference, database down)
- Try-catch pattern:
try { await service.Create(...) } catch (ValidationException ex) { return BadRequest(ex.Message) } - HTTP 400 Bad Request: Client error (invalid input, missing required field, duplicate key)
- HTTP 401 Unauthorized: User not authenticated (no login, invalid token)
- HTTP 403 Forbidden: User authenticated but lacks permission (student can't delete other students)
- HTTP 404 Not Found: Resource doesn't exist (GET /students/999 when 999 not in database)
- HTTP 500 Internal Server Error: Unexpected server error (null reference, database connection failed)
- ActionResults for errors:
BadRequest(error)(400),NotFound()(404),Forbid()(403),Unauthorized()(401) - Global exception handler middleware: Catches all unhandled exceptions, logs them, returns 500 to client
- ProblemDetails RFC 7807: Standardized error format: status code, error type, title, detail fields
- Logging exceptions:
_logger.LogError(ex, "Failed to create student")includes exception details for debugging - Custom exception classes:
throw new StudentNotFoundException("Student 101 not found")for specific errors - School Management error scenarios: Student not found (404), invalid class number (400), unauthorized access (403), database error (500)
- Error response to client: Generic message in production ("An error occurred"), detailed in development (stack trace)
Basic Try-Catch
[HttpPost]
public async Task<ActionResult<Student>> CreateStudent([FromBody] Student student)
{
try
{
await _service.CreateStudentAsync(student);
return CreatedAtAction(nameof(GetStudent), new { id = student.Id }, student);
}
catch (ArgumentException ex)
{
return BadRequest(new { error = ex.Message }); // 400
}
catch (Exception ex)
{
return StatusCode(500, new { error = "Internal server error" }); // 500
}
}
HTTP Response Codes
| Code | Scenario |
|---|---|
| 200 | Success |
| 201 | Created |
| 400 | Validation error |
| 401 | Not authenticated |
| 403 | Not authorized |
| 404 | Not found |
| 409 | Conflict (duplicate) |
| 500 | Server error |
Problem Details Response
[HttpGet("{id}")]
public async Task<ActionResult<Student>> GetStudent(int id)
{
var student = await _service.GetStudentAsync(id);
if (student == null)
{
return NotFound(new ProblemDetails
{
Title = "Student Not Found",
Detail = $"Student with ID {id} does not exist",
Status = StatusCodes.Status404NotFound
});
}
return Ok(student);
}
Response:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Student Not Found",
"status": 404,
"detail": "Student with ID 101 does not exist"
}
Global Exception Handler
File: Middleware/ExceptionHandlingMiddleware.cs
public class ExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ExceptionHandlingMiddleware> _logger;
public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsJsonAsync(new
{
error = "An unexpected error occurred",
detail = ex.Message
});
}
}
}
// Register in Program.cs
app.UseMiddleware<ExceptionHandlingMiddleware>();
Custom Exceptions
public class StudentNotFoundException : Exception
{
public StudentNotFoundException(int id)
: base($"Student with ID {id} not found") { }
}
public class DuplicateRollNumberException : Exception
{
public DuplicateRollNumberException(string rollNumber)
: base($"Roll number {rollNumber} already exists") { }
}
// Usage in controller
[HttpPost]
public async Task<ActionResult<Student>> CreateStudent([FromBody] Student student)
{
try
{
return CreatedAtAction(nameof(GetStudent), await _service.CreateStudentAsync(student));
}
catch (DuplicateRollNumberException ex)
{
return Conflict(new { error = ex.Message }); // 409
}
catch (ArgumentException ex)
{
return BadRequest(new { error = ex.Message }); // 400
}
}
Logging Errors
[HttpPost]
public async Task<ActionResult<Student>> CreateStudent([FromBody] Student student)
{
try
{
_logger.LogInformation("Creating student: {Name}", student.Name);
var result = await _service.CreateStudentAsync(student);
_logger.LogInformation("Student created: {Id}", result.Id);
return CreatedAtAction(nameof(GetStudent), new { id = result.Id }, result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating student");
return StatusCode(500, new { error = "Failed to create student" });
}
}
Key Takeaways
- Always catch exceptions
- Return appropriate HTTP status
- Log for debugging
- Don't expose internal details
- Use Problem Details format
💡 Error Handling Tip
Log errors, but don't expose stack traces to client.
⚠️ Common Mistakes
- No error handling — Unhandled exceptions crash API
- Returning 500 for validation — Use 400
- Logging stack traces to client — Security risk
- No logging — Can't debug issues
🤖Use AI to Learn Faster
Use ChatGPT, Claude, or Copilot to go deeper on Error Handling. Try these prompts:
"When do I use 400 vs 500?""How do I create custom exceptions?""What's a global exception handler?""Quiz me on error handling"
💡 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.
nexcoding.in
Have questions on your tech stack, ongoing projects, or need one-to-one training?