Skip to main content

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

CodeScenario
200Success
201Created
400Validation error
401Not authenticated
403Not authorized
404Not found
409Conflict (duplicate)
500Server 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
  1. No error handling — Unhandled exceptions crash API
  2. Returning 500 for validation — Use 400
  3. Logging stack traces to client — Security risk
  4. 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