Global Exception Handling
Exceptions will happen. Global exception handling makes sure every one of them is logged, mapped to the right status code, and never leaks a stack trace to the client.
On this page · 7 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Map known exception types to status codes with an exception filter
- Return one safe 500 response with IExceptionHandler
- Log every exception with IExceptionLogger
- Correlate a client-visible error ID with the log
Explanation#
Without global handling, try/catch blocks repeat in every action, and unexpected errors return Web API’s default 500 — which can include exception details.
An ExceptionFilterAttribute catches exceptions thrown by actions. Use it to translate your own exception types: NotFoundException → 404, BusinessRuleException → 409.
IExceptionHandler (derive from ExceptionHandler) decides the response for anything still unhandled — including errors outside actions, such as in message handlers or serialisation. There is only one handler; you replace the default.
IExceptionLogger (derive from ExceptionLogger) is called for every exception, before filters and the handler. There can be several loggers.
Give each failure a short error ID, write it to the log and return it to the client. Support staff can find the exact log entry from a customer’s screenshot.
Set config.IncludeErrorDetailPolicy = LocalOnly (or Never) so details are hidden from remote clients.
Syntax#
config.Filters.Add(new DomainExceptionFilterAttribute()); // known errors
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler()); // everything else
config.Services.Add(typeof(IExceptionLogger), new TraceExceptionLogger()); // log all
Code example: Filter, handler and logger working together#
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.ExceptionHandling;
using System.Web.Http.Filters;
using System.Web.Http.Results;
public class NotFoundException : Exception
{
public NotFoundException(string message) : base(message) { }
}
public class BusinessRuleException : Exception
{
public BusinessRuleException(string message) : base(message) { }
}
// 1. Log every exception and give it an id
public class TraceExceptionLogger : ExceptionLogger
{
public override void Log(ExceptionLoggerContext context)
{
var errorId = Guid.NewGuid().ToString("N").Substring(0, 8);
context.Request.Properties["ErrorId"] = errorId;
Trace.TraceError("[{0}] {1} {2}: {3}", errorId, context.Request.Method,
context.Request.RequestUri, context.Exception);
}
}
// 2. Known exceptions → specific status codes
public class DomainExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
var ex = context.Exception;
if (ex is NotFoundException)
context.Response = context.Request.CreateResponse(HttpStatusCode.NotFound, new { message = ex.Message });
else if (ex is BusinessRuleException)
context.Response = context.Request.CreateResponse(HttpStatusCode.Conflict, new { message = ex.Message });
}
}
// 3. Everything else → one safe 500
public class GlobalExceptionHandler : ExceptionHandler
{
public override void Handle(ExceptionHandlerContext context)
{
object errorId;
context.Request.Properties.TryGetValue("ErrorId", out errorId);
var response = context.Request.CreateResponse(HttpStatusCode.InternalServerError, new
{
message = "Something went wrong. Please try again.",
errorId,
});
context.Result = new ResponseMessageResult(response);
}
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.Filters.Add(new DomainExceptionFilterAttribute());
config.Services.Replace(typeof(IExceptionHandler), new GlobalExceptionHandler());
config.Services.Add(typeof(IExceptionLogger), new TraceExceptionLogger());
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.LocalOnly;
}
}
public class ProductsController : ApiController
{
[HttpGet, Route("api/products/{id:int}")]
public IHttpActionResult Get(int id)
{
if (id == 0) throw new InvalidOperationException("Connection to SQL Server failed");
if (id > 100) throw new NotFoundException("Product " + id + " was not found.");
return Ok(new { Id = id, Name = "Pen" });
}
}
GET /api/products/5 → 200 {"Id":5,"Name":"Pen"}
GET /api/products/500 → 404 {"message":"Product 500 was not found."}
GET /api/products/0 → 500 {"message":"Something went wrong. Please try again.","errorId":"3f9c2a1b"}
Log: [3f9c2a1b] GET http://shop/api/products/0: System.InvalidOperationException:
Connection to SQL Server failed at ProductsController.Get(Int32 id) ...This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- The logger runs first for every exception, creates an error ID and stores it on the request.
- The exception filter turns NotFoundException into 404 and BusinessRuleException into 409.
- Anything the filter did not handle reaches GlobalExceptionHandler, which returns a generic 500 with the error ID.
- The client never sees the real message (“Connection to SQL Server failed”) — only the log does.
- Actions stay clean: they just throw meaningful exceptions.
Real-world use case#
A customer reports “error 3f9c2a1b” from the mobile app. Support searches the logs for that ID and finds the exact request, user and stack trace in seconds.
Common mistakes#
Database and file-system messages reveal server names, table names and paths. Return a generic message.
Filters do not catch errors from message handlers, routing or serialisation. Add an IExceptionHandler for those.
catch { } in an action hides failures from both the client and the logs. Let exceptions reach the global handler.
Best practices#
- Throw specific exception types from services
- Map them centrally with one filter
- One handler for a safe 500 with an error ID
- Never send stack traces to remote clients
Remember this#
Logger: log everything. Filter: known errors → 4xx. Handler: the rest → safe 500.
Error ID in the response = error ID in the log.
IncludeErrorDetailPolicy.LocalOnly in production.
Practice exercise#
Your task: Extend the exception filter so that UnauthorizedAccessException returns 403 with {"message":"You do not have access to this resource."}.
Need a hint?
Show solution
using System;
using System.Net;
using System.Net.Http;
using System.Web.Http.Filters;
public class AccessExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
if (context.Exception is UnauthorizedAccessException)
{
context.Response = context.Request.CreateResponse(HttpStatusCode.Forbidden,
new { message = "You do not have access to this resource." });
}
}
}
Mini quiz#
Which component can catch exceptions thrown outside controller actions, e.g. in a message handler?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.