Back to: ASP.NET Core Web API Tutorials
Error Handling and Logging in ASP.NET Core Minimal API
In this article, I will discuss how to implement Error Handling and Logging in ASP.NET Core Minimal API with examples. Please read our previous articles discussing how to implement a Minimal API in ASP.NET Core with Examples. We will be working with the same project that we worked on so far with ASP.NET Core Minimal API.
Error Handling and Logging in ASP.NET Core Minimal API
Error handling and logging are crucial aspects of building robust, maintainable, and scalable APIs. Effective error handling ensures that users receive clear, actionable responses when an error occurs, while logging provides invaluable insights into the internal workings of your application, helping developers diagnose and resolve issues.
What is Error Handling?
Error handling refers to the set of techniques and practices we use to deal with unexpected situations or failures that occur while our API is running. This includes issues such as invalid input, missing resources, exceptions in business logic, or failures in external dependencies (e.g., a database or web service).
Why is Error Handling Important?
- Prevents Application Crashes: Proper error handling ensures that your API does not crash unexpectedly when exceptions occur.
- User Experience: Clients (frontend apps, other APIs) receive clear, meaningful, and consistent error responses rather than raw stack traces or cryptic messages.
- Enhances Security: Prevents sensitive information from being leaked by controlling the error details sent to clients.
- Maintainability: Well-structured error handling makes debugging and maintenance easier, as it enables you to pinpoint the cause of failures quickly.
What is Logging?
Logging is the process of recording information about our application’s runtime behaviour, such as events, warnings, errors, or general traces, into a persistent store (such as files, databases, or logging services). This information is crucial for monitoring, troubleshooting, auditing, and analyzing the health and usage of our application.
Why Is Logging Important?
- Records Runtime Information: Logs capture essential events, errors, warnings, and informational messages during application execution.
- Facilitates Debugging: Logs help developers trace the flow of execution and identify root causes of problems.
- Supports Monitoring: Logging integrates with monitoring and alerting tools, enabling proactive issue detection.
Global Exception Handling Middleware
First, create a new class file named ErrorHandlerMiddleware.cs within the Models folder, and then copy and paste the following code. The following ErrorHandlerMiddleware provides global error handling for the application. It catches unhandled exceptions that occur during the processing of HTTP requests, logs the error details, and returns a consistent JSON response to the client.
using System.Net; using System.Text.Json; namespace MinimalAPIDemo.Models { public class ErrorHandlerMiddleware { // Holds the next middleware in the pipeline to invoke private readonly RequestDelegate _next; // Logger instance for logging errors private readonly ILogger<ErrorHandlerMiddleware> _logger; // Constructor injects the next middleware and a logger public ErrorHandlerMiddleware(RequestDelegate next, ILogger<ErrorHandlerMiddleware> logger) { _next = next; // Assign the next middleware in the chain _logger = logger; // Assign the injected logger } // This method is called for every HTTP request. Handles errors during the request processing. public async Task InvokeAsync(HttpContext context) { try { // Invoke the next middleware in the pipeline; await in case it's asynchronous await _next(context); } catch (Exception ex) { // Log the exception with its stack trace and a message _logger.LogError(ex, "An unhandled exception has occurred."); // Set response content type as JSON because we will return a JSON error response context.Response.ContentType = "application/json"; // Set HTTP status code to 500 (Internal Server Error) context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; // Create an anonymous object representing the error details var response = new { Title = "An unexpected error occurred.", // User-friendly Short title of the error Status = context.Response.StatusCode, // Include HTTP status code (500) // Conditionally include detailed error message if environment is Development Detail = context.RequestServices.GetService(typeof(IWebHostEnvironment)) is IWebHostEnvironment env && env.IsDevelopment() ? ex.Message // Show detailed exception message in Development : "Please contact support." // Generic message in Production or other environments }; // Serialize the anonymous error object into a JSON string var jsonResponse = JsonSerializer.Serialize(response); // Write the JSON error response to the HTTP response body asynchronously await context.Response.WriteAsync(jsonResponse); } } } }
Code Explanation:
The middleware intercepts every HTTP request. It executes the next delegate in the pipeline inside a try block. If an exception is thrown, it is caught and logged. A structured JSON response with status, title, and detail is returned. Detailed error messages are only displayed in the Development environment to prevent the exposure of sensitive information in production. This approach centralizes error handling in one place, resulting in cleaner code and consistent error responses.
Configure Logging
Please modify the appsettings.json file as follows.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }
Implementing Error Handling and Logging in ASP.NET Core Minimal API
The following example demonstrates how to implement robust logging and structured error handling in an ASP.NET Core Minimal API application. It shows how to use the built-in logging framework to record information, warnings, and errors for each API operation, and how to ensure that all exceptions, whether anticipated (such as validation failures or missing data) or unexpected (such as runtime errors), are consistently handled and reported.
By wrapping endpoint logic in try-catch blocks and returning standardized error responses using Results.Problem(), as well as registering a global error handling middleware, the application ensures clear diagnostics for developers and provides safe, uniform, and informative error feedback to API consumers. So, please modify the Program class as follows:
using MinimalAPIDemo.Models; namespace MinimalAPIDemo { public class Program { public static void Main(string[] args) { // Create the WebApplication builder which prepares the app with default configs var builder = WebApplication.CreateBuilder(args); // Configure logging providers builder.Logging.ClearProviders(); // Remove any default logging providers builder.Logging.AddConsole(); // Add console logger (shows logs in terminal) builder.Logging.AddDebug(); // Add debug logger (for IDE/debugging tools) // Configure JSON serialization options for HTTP responses builder.Services.ConfigureHttpJsonOptions(options => { // Disable camelCase conversion; keep property names as declared (PascalCase) options.SerializerOptions.PropertyNamingPolicy = null; }); // Add Swagger/OpenAPI support services for API documentation and UI generation builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Register the EmployeeRepository as a singleton service in DI container // This means one instance will be shared across the app lifetime builder.Services.AddSingleton<IEmployeeRepository, EmployeeRepository>(); // Build the app (finalize service registration and middleware pipeline) var app = builder.Build(); // Enable Swagger UI only in Development environment to test API endpoints interactively if (app.Environment.IsDevelopment()) { app.UseSwagger(); // Enable Swagger middleware to generate swagger.json app.UseSwaggerUI(); // Enable Swagger UI middleware to visualize API docs } // Register the custom global error handling middleware in the pipeline // It will catch exceptions from downstream middleware/endpoints app.UseMiddleware<ErrorHandlerMiddleware>(); // -------------------- Define Minimal API Endpoints with try-catch -------------------- // GET /employees - Fetch all employees app.MapGet("/employees", (IEmployeeRepository repo, ILogger<Program> logger, HttpContext httpContext) => { try { logger.LogInformation("Fetching all employees"); // Example: Simulate error if query parameter "causeError" is true var query = httpContext.Request.Query; if (query.ContainsKey("causeError") && bool.TryParse(query["causeError"], out bool causeError) && causeError) { // Simulate a null reference exception throw new NullReferenceException("Simulated null reference exception for testing."); } var employees = repo.GetAllEmployees(); return Results.Ok(employees); // Return HTTP 200 with employee list } catch (Exception ex) { // Log exception details as Error level logger.LogError(ex, "Error occurred while fetching all employees"); // Return a consistent HTTP 500 problem JSON response return Results.Problem( detail: "An error occurred while processing your request.", statusCode: 500, instance: httpContext.Request.Path, title: "Internal Server Error"); } }); // GET /employees/{id} - Fetch employee by ID app.MapGet("/employees/{id}", (int id, IEmployeeRepository repo, ILogger<Program> logger) => { try { logger.LogInformation($"Fetching employee with ID: {id}"); var employee = repo.GetEmployeeById(id); if (employee == null) { // Log warning if employee not found logger.LogWarning($"Employee with ID {id} not found"); // Return HTTP 404 with message return Results.NotFound(new { Message = $"Employee with ID {id} not found." }); } return Results.Ok(employee); // Return HTTP 200 with employee details } catch (Exception ex) { logger.LogError(ex, $"Error occurred while fetching employee with ID {id}"); return Results.Problem( detail: "An error occurred while processing your request.", statusCode: 500, title: "Internal Server Error"); } }); // POST /employees - Create a new employee app.MapPost("/employees", (Employee newEmployee, IEmployeeRepository repo, ILogger<Program> logger) => { try { // Validate incoming employee data if (!ValidationHelper.TryValidate(newEmployee, out var errors)) { logger.LogWarning($"Validation failed for new employee: {string.Join(", ", errors.Select(e => e.ErrorMessage))}"); // Return HTTP 400 Bad Request with validation errors return Results.BadRequest(new { Message = "Validation Failed", Errors = errors.Select(e => e.ErrorMessage) }); } // Add the new employee record var createdEmployee = repo.AddEmployee(newEmployee); logger.LogInformation($"Employee created with ID {createdEmployee.Id}"); // Return HTTP 201 Created with location header return Results.Created($"/employees/{createdEmployee.Id}", createdEmployee); } catch (Exception ex) { logger.LogError(ex, "Error occurred while creating a new employee"); return Results.Problem( detail: "An error occurred while processing your request.", statusCode: 500, title: "Internal Server Error"); } }); // PUT /employees/{id} - Update an existing employee app.MapPut("/employees/{id}", (int id, Employee updatedEmployee, IEmployeeRepository repo, ILogger<Program> logger) => { try { // Validate updated data if (!ValidationHelper.TryValidate(updatedEmployee, out var errors)) { logger.LogWarning($"Validation failed while updating employee {id}: {string.Join(", ", errors.Select(e => e.ErrorMessage))}"); return Results.BadRequest(new { Message = "Validation Failed", Errors = errors.Select(e => e.ErrorMessage) }); } // Update employee if exists var employee = repo.UpdateEmployee(id, updatedEmployee); if (employee == null) { logger.LogWarning($"Attempted to update non-existent employee with ID {id}"); return Results.NotFound(new { Message = $"Employee with ID {id} not found." }); } logger.LogInformation($"Employee with ID {id} updated"); return Results.Ok(employee); } catch (Exception ex) { logger.LogError(ex, $"Error occurred while updating employee with ID {id}"); return Results.Problem( detail: "An error occurred while processing your request.", statusCode: 500, title: "Internal Server Error"); } }); // DELETE /employees/{id} - Delete an employee by ID app.MapDelete("/employees/{id}", (int id, IEmployeeRepository repo, ILogger<Program> logger) => { try { var deleted = repo.DeleteEmployee(id); if (!deleted) { logger.LogWarning($"Attempted to delete non-existent employee with ID {id}"); return Results.NotFound(new { Message = $"Employee with ID {id} not found." }); } logger.LogInformation("Employee with ID {EmployeeId} deleted", id); return Results.NoContent(); // HTTP 204 No Content on successful deletion } catch (Exception ex) { logger.LogError(ex, $"Error occurred while deleting employee with ID {id}"); return Results.Problem( detail: "An error occurred while processing your request.", statusCode: 500, title: "Internal Server Error"); } }); // Start the web server and listen for incoming HTTP requests app.Run(); } } }
How Global Error Handling Works Here
Global error handling middleware acts as a safety net, catching any “unhandled” exceptions in the pipeline and ensuring our API always responds if something unexpected goes wrong. It will work as follows:
- Custom Middleware Registration: The line app.UseMiddleware<ErrorHandlerMiddleware>(); registers a custom middleware that wraps the entire request pipeline after it is added. This middleware sits early in the pipeline and intercepts all HTTP requests.
- Exception Capture: When a request enters the middleware, it attempts to invoke the next component (await _next(context);). If downstream middleware or your endpoint throws an unhandled exception, the control jumps to the middleware’s catch block.
- Logging the Exception: The middleware logs the full exception details with _logger.LogError(…). This ensures that all errors are captured centrally, eliminating the need for individual try-catch blocks everywhere.
- Custom Response to Clients: Instead of the application crashing or returning generic server errors, the middleware sends a clean, consistent JSON response with an HTTP 500 status and an error message (in accordance with the “Problem Details” standard).
What is Results.Problem?
The Results.Problem method is a helper method provided by ASP.NET Core Minimal APIs to create a standardized ProblemDetails response. ProblemDetails provides a structured way to return error information. Instead of manually creating JSON error responses for every API failure, you can use Results.Problem() to automatically produce a consistent, structured, and meaningful error payload that clients can easily understand and parse.
The ProblemDetails object returned by Results.Problem() typically includes the following properties:
- Status (int): This is the HTTP status code representing the error condition (e.g., 400, 404, 500). It tells the client the general class of error encountered.
- Detail (string): A human-readable explanation about the error. This helps developers or users understand the cause or nature of the problem.
- Title (string): A brief, human-readable summary or title for the error. This is usually a short phrase describing the type of error (e.g., “Not Found”, “Internal Server Error”).
- Type (string, optional): A URI reference that points to human-readable documentation about the error type. This is useful for linking to more detailed error documentation.
- Instance (string, optional): A URI reference that identifies the specific occurrence of the error, for example, the URL of the request or resource that caused the error.
Implementing robust error handling and logging in Minimal APIs significantly enhances application reliability, maintainability, and debugging ease. These practices not only help you catch issues early but also provide clients with reliable, informative feedback, making your API professional and production-ready.
In the next article, I will discuss how to Implement Asynchronous Programming in ASP.NET Core Minimal API with Examples. In this article, I explain how to Implement Error Handling and Logging in ASP.NET Core Minimal API with Examples. I hope you enjoy this article, Error Handling and Logging in ASP.NET Core Minimal API.