Clean Architecture — Best Exception Handling with Consistent Responses in ASP.NET Core API

Discussing how exceptions can be handled in a centralized spot and how to return consistent responses. Exception logging is also centralized in the same approach.

Shawn Shi
5 min readJan 12, 2021

UPDATE April 10, 2022: all projects in the GitHub repo have been upgraded to .NET 5.


I adopted a REST API project today which had no exception handling or logging, making it really hard to investigate issues. So I spent an hour adding a centralized exception handler and structured logging using Serilog. In this article, I’d like to discuss how exception handling and logging can be setup at the beginning phase of a project following a best and easy concept.


Once the business-level entities are defined, common exceptions and business-specific exceptions should be defined so that they can be shared among projects like API, Function App, Console App, etc..

Once the exceptions are defined, we need a centralized spot to handle all the pre-defined exceptions as well as the unknown exceptions (most likely a 500 error), log the errors, and then return a consistent API response so that any client that calls the API knows what to expect.

Define Exceptions

For example, let’s define the following types of exceptions that will be shared by any projects referencing the Core project.

For example, the EntityNotFoundException is defined as follows:

Add Filter

In order to catch all the exceptions, we can define a filter that runs asynchronously after an action has thrown an exception.

For example, the following filter has a list of handlers to handle different types of exceptions, and also a default handler that will handle everything else.

Notice an internal dictionary _exceptionHandlers is used to register all the known exceptions and their specific handlers. If a handler is found for a specific exception, it will be used to produce the response. Otherwise, the default handler will handle all exceptions.

Response like NotFoundObjectResult, BadRequestObjectResult inherit from the base class ObjectResult, which defines the response as an object with Type, Title, and Detail properties.

Logging is also done here so that every exception gets logged into whatever sink you’ve defined when you register logging service. This allows the developer to safely throw exceptions without worrying about logging.

Register Filter

Register the newly created filter to the MvcOptions like below when you call AddControllers() or AddMvc().

See Exception Handling in Action!

Testing entity not found exception either by manually throwing an exception in a test API endpoint.

Or, a more realistic entity not found exception is probably thrown when the user attempts to retrieve an entity that does not exist. For example, in my Get query handler using MediatR Query/Command pattern below on line 79, EntityNotFoundException is thrown when an entity isn’t found. This allows my controller to SAFELY assume the query response will never be null and SAFELY return a strongly typed result, ToDoItemModel, with a 200 status code.

See the controller action method for the Get query above. Another benefit of handling exceptions in this approach is that my Controller code is extremely clean, 2 lines, and handles no business logic! Perfect for the controllers!

The error response is structured like below. The same response structure shall be returned by other exception handlers and the unknown exception handlers, so that any clients like mobile app or React app can easily handles failed requests following a consistent manner.

Notice logging also automatically happened both in the Console and Rolling File as I have registered for Serilog.

Similarly, when an Entity Already Exists exception is thrown, it will get caught by our filter and the response will be structured as below:


Congratulations! In less than an hour, you probably have setup a centralized exception handling spot and also logging.

It is pretty easy and straightforward to setup centralized exception handling and logging. This is probably best done at the beginning of a project so that all peer developers follow the same approach. Any newly defined exceptions can be added and handled in a consistent manner.

The sample code in this article is from a GitHub starter project, which uses Clean Architecture to organize the projects. This project features Partitioned Repository Pattern using Azure Cosmos DB to build scalable backend service, REST API, Azure Functions, React SPA, etc.. Feel free to use the whole starter project or part of it to kick start your next exciting adventure!

For more resources relevant to the project:



Shawn Shi

Senior Software Engineer at Microsoft. Ex-Machine Learning Engineer. When I am not building applications, I am playing with my kids or outside rock climbing!