Skip to main content
Software Alchemy

The Art and Science of Software Development

Scaffold Your Clean DDD Web Application - Part 4: Global Exception Handling in ASP.NET Core and Angular

Global Exception Handling in ASP.NET Core and Angular

Image by Pexels from Pixabay

Introduction

In the previous blog entry, I discussed the data model and our strategy for persisting/reading data to/from the database in a way that facilitates the CQRS architectural pattern. I also discussed combining Dapper with EF Core for maximum flexibility, and we briefly revisited the high-level tenancy models.

In this blog entry I’ll go over using custom ASP.NET Core middleware to catch various kinds of exceptions so that we can return the proper HTTP status code to the user’s browser. At the Angular side we’ll take advantage of these status codes in order to direct the user to the appropriate pages on an error—an "unauthorized" page or general error page—or simply display a notification. This is one approach to handling server exceptions which are prone to "bubble up" from lower layers of the stack, such as the Application and Domain layers, and elegantly updating the UI.

As before, I’ve made sweeping changes to our demo application that you can access on GitHub here. I encourage you to follow along in the code and if you have any feedback or suggestions, feel free to let me know.

Exception Flow

Before we jump in, let’s revisit the high-level architecture you’ve seen before, if you’ve been reading previous blog entries.

High Level Clean DDD Architecture

The big takeaway from this simple diagram is that this represents the Dependency Inversion Principle on an architectural level.

The Dependency Inversion Principle states:

High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).

Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

I’ve spoken about this principle in previous blog entries, so you may want to revisit those if you’re starting to get lost. Now, pay attention here, this is important. As Uncle Bob points out in his influential blog article simply entitled The Clean Architecture you’ll notice that the dependencies point inward, like you see below:

Clean DDD Architecture Dependency Direction

Our architecture abides by Bob’s "Dependency Rule," which is to say that peripheral layers rely upon core layers, and Dependency Injection is the magic glue that ties them together:

  • Infrastructure and Persistence models and interfaces are declared in the Application layer. Concrete implementations of these interfaces are found in the peripheral layers and get injected into the orchestration components of the Application layer.
  • The Presentation layer executes controller actions, representing high-level operations, against Application layer services. This is accomplished using the CQRS pattern, facilitated by MediatR.
  • Domain (business) operations are orchestrated directly by the Application layer. For the most part, Dependency Inversion/IOC is NOT used to instantiate Domain layer components, for the simple, yet elusive reason that everything going on in there is already abstract—it represents the realm of pure business logic. It is truly the core of the core of this architecture.

You might believe that when something goes wrong (database components start throwing exceptions, a message queue is down, business logic has logic errors, so on) that this diagram would be completely inverted. However, this is not so. As you can see below...

Clean DDD Architecture Exception Flow

... aside from the fact that our new diagram looks kind of like the Flux Capacitor from the Back to the Future movies, under error conditions exceptions don’t necessarily flow against the direction of dependency. Here’s why.

  • The Web API is still the entry-point for our application. This is where all dependencies get wired up and resolved by our IOC container. It’s effectively the top of the stack, were you to take this diagram and flatten it out.
  • Even though we are programming against abstractions, those abstractions need to have something concrete behind them at the end of the day. Those low-level/external components get called by the Application layer, which is the lynch pin that holds the architecture together.

Bridge from the Web API to the UI

As the diagram above implies, our goal is to handle exceptional, error condition scenarios in the Web API and communicate those errors to the UI, namely by serializing the exceptions as JSON text and setting the proper HTTP response code for the web response. Using industry standards to communicate errors across the wire isn’t exactly a revolutionary idea on my part, but there are some nuances here that I’d like to discuss that aren’t agreed upon by all web developers. Taking a step back, please allow me to share my general philosophy on best practices for handling exceptions in an enterprise system. My opinion may differ from that of other software professionals out in the blogosphere, and in the end it’s up to you to decide what patterns and practices work best for you. At any rate, here is my stance:

  1. Whenever possible, it is best to handle abnormal conditions such as validation failures and/or authorization failures as high up in the stack as possible—in other words in the Web API controller methods. This should not come as a surprise to anyone. If possible, a declarative approach, such as using ASP.NET Core authorization filters and policies should be preferred.
  2. Exceptions are perfectly OK to throw inside deeper layers of the application—i.e. the Application layer, Domain layer, etc.—especially when used for business logic errors and so forth. However, be aware that this comes at a cost, and that exceptions can be computationally expensive to handle in your application.
  3. Custom exception types should have semantic names which are relevant to the application, and not be coerced by the technology stack you’re building on or the protocol you’re using (HTTP). I’ll explain more below.
  4. Exceptions which are thrown by deeper layers of the application should be caught, logged, and communicated to the UI by the Web API, however...
  5. As developers, we should be diligent NOT to get into the habit of using exceptions for flow control in our application (i.e. IF-THEN-ELSE logic). Exceptions are for exceptional, unexpected error conditions.

In essence, we are walking a fine line between two extremes:

  • Preemptively packing all error checking logic into top-level methods of high-level operations—Web API calls—in order to avoid throwing exceptions. This is NOT recommended and is futile to boot, because no software developer is omniscient enough to anticipate every single possible error scenario for sufficiently complex applications, and even making the attempt to do so would result in controller methods that are bloated and unmaintainable. Furthermore, this flies in the face of our Clean DDD architectural approach, which aims to encapsulate business logic in the core of our application. This includes handling business logic failure conditions.
  • Throwing exceptions willy-nilly all throughout the stack and gleefully using them for flow control. This is also NOT recommended, because it results in a different kind of unmaintainability. Exceptions are special in that they bubble up the call stack, effectively cutting across layers of the application. This characteristic should not be abused.

We will walk the middle path, in which:

  • As much as possible, validation and authorization checks are done at the controller level. This can be done inside custom filter attributes, authorization policies, or authorization handlers.
  • When exceptions bubble up from deeper layers of the application, these will be caught by middleware and translated into the proper HTTP error responses that the UI can handle.

Some solutions may implement custom exception types which correspond to the standard HTTP status codes. For instance, one may declare exception types such as BadRequestException, ForbiddenException, etc. I generally disagree with this approach and here’s why: in the context of a particular SaaS application, these are broadly defined and don’t really mean anything. The various verbs and status codes of the HTTP standard were intentionally designed to be general purpose because they’re meant to accommodate a wide swath of potential applications. However, that inherent vagueness doesn’t mesh well with the bounded contexts and ubiquitous language of our particular application.

In our OrgManager solution, for instance, does it make sense for the Domain layer to throw a BadRequestException when an employee tries to request more time off than they have accrued, or would it be easier to understand, debug, and maintain a section of code that instead threw something like a TimeOffException, or EmployeeException? The latter is what I’ve tried to implement in the demo application (though in some situations, like with the "Not Found" status code, the HTTP standard name is perfectly fine).

If you examine the demo solution, you’ll see that what I’ve done is fairly straightforward:

A base exception type is declared for each core layer of the application—ApplicationLayerException for the Application layer and DomainLayerException for the domain layer.

Additional, more specific exception types are derived from those two in areas of the application in which they make sense. For instance, the Domain layer has an EmployeeException under the Human Resources bounded context. The Application layer declares certain functionally oriented exception types that are appropriate inside CQRS workflows, such as AccessDeniedException, NotFoundException, etc.

When creating new exception types, one of the options available under the quick actions context menu (CTRL+.) is "Generate All." This will generate all constructors for your custom exception type, and I highly recommend you do that. The result looks something like this:


    public DomainLayerException()
    {
    }

    public DomainLayerException(SerializationInfo info, StreamingContext context) : base(info, context)
    {
    }

    public DomainLayerException(string message) : base(message)
    {
    }

    public DomainLayerException(string message, Exception innerException) : base(message, innerException)
    {
    }

Handling Exceptions with Middleware

Once we’ve declared our custom exception types, we need a way to catch any such exceptions that get thrown further down the stack and translate them into the proper HTTP response.

In the demo solution I created a simple test controller to simulate these different exception types being thrown, so that we can determine if our middleware is working the way we’d expect it to. I created an Angular component to call those endpoints, which is what you see below.

Angular Exception Generator

By default, ASP.NET Core will catch any unhandled exceptions and return an HTTP status 500 to the client, like what you see here:

Generating 500

Examining a response while running in Development mode shows a stack trace...

Stack Trace

... while running in Production mode shows a general "error processing your request" response.

Production Error Message

We’ll change this default behavior, so that more appropriate error codes are returned for a given exception, and details of the error that occurred are serialized as JSON, which our UI can more seamlessly utilize.

There are a few different ways to tackle the problem at hand. I’ll demonstrate two of them. In the first approach we’ll create and wire in our own custom middleware component and in the second we’ll hook in to the existing ASP.NET Core exception handling middleware.

Before going further, it’d be worth taking a few minutes to review how the ASP.NET Core middleware pipeline works. This is a good place to start.

In simplistic terms, the ASP.NET Core middleware pipeline represents the classic two-pass Chain of Responsibility pattern, like what you see here:

Middleware Pipeline

Each middleware component gets a chance to handle both the inbound request and outbound response, and short-circuit the pipeline if appropriate. This is exceptionally useful (no pun intended) when handling error conditions, and my hat’s off to the ASP.NET Core team for choosing this design pattern.

Approach #1: Creating Our Own Custom Exception-Handling Middleware

The first way is the simplest to understand, and you’re likely to find this across different blogs and Stack Overflow questions on the Net. We simply create our own middleware component like so:


public class CustomErrorHandlingMiddleware
{
    private readonly RequestDelegate next;

    public CustomErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context, IWebHostEnvironment env)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex, env);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex, IWebHostEnvironment env)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if (ex is AccessDeniedException)
        {
            code = HttpStatusCode.Forbidden;
        }
        if (ex is AuthorizationException)
        {
            code = HttpStatusCode.Unauthorized;
        }
        else if (ex is NotFoundException)
        {
            code = HttpStatusCode.NotFound;
        }
        else if (ex is ApplicationLayerException)
        {
            code = HttpStatusCode.BadRequest;
        }

        var includeDetails = env.IsEnvironment("Development");
        var title = includeDetails ? "An error occurred: " + ex.Message : "An error occurred";
        var details = includeDetails ? ex.ToString() : null;

        var problem = new ProblemDetails
        {
            Status = (int?)code,
            Title = title,
            Detail = details
        };

        context.Response.ContentType = "application/problem+json";
        context.Response.StatusCode = (int)code;

        var result = JsonConvert.SerializeObject(problem);
        return context.Response.WriteAsync(result);
    }
}

The way it works is dirt simple. The body of the Invoke() method, which gets called by the ASP.NET Core pipeline, contains a try/catch block around the request delegate invocation. Any exceptions which get thrown are caught and handled inside the private HandleExceptionAsync() method. This method examines the type of the exception and based upon that, sets the appropriate HTTP status code. Details of the exception are serialized as a ProblemDetails object so that it abides by industry standards. Done and done.

In keeping with our practice of using a DependencyInjectionExtensions static class to simplify configuration, we add one more extension method to that class.


public static IApplicationBuilder UseCustomErrorHandlingMiddleware(this IApplicationBuilder app) =>
    app.UseMiddleware<CustomErrorHandlingMiddleware>();

When wiring it into the pipeline in the Startup class, this should be right near the top of the Configure() method.


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // API error handling.
    // Approach 1: custom error handling middleware.
    app.UseCustomErrorHandlingMiddleware();
...

When we again generate custom exceptions using our test error controller, the results are quite different.

HTTP Response Codes

As you can see, the proper response codes are being returned...

HTTP Response JSON

... and the body of the response contains the JSON we would expect.

Hooray!

Approach #2: Hooking into ASP.NET Core ExceptionHandlerMiddleware

As Andrew Lock, author of ASP.NET Core in Action states in this blog post, there are a few edge cases which can make it dicey to roll your own global exception handling middleware. For that reason, I’ll now present the approach he recommends.

Honestly, what you see here is just a modified version of Andrew’s code, so I recommend you give his blog entry a read. The big difference between this one and Approach #1 is that instead of creating middleware, we are declaring the exception handling functionality as methods on a static class, which will then get hooked into the out-of-the-box ASP.NET Core exception handling middleware. Our static class looks like this:


public static class CustomErrorHandlerHelper
{
    public static Task WriteDevelopmentResponse(HttpContext httpContext, Func<Task> next)
        => WriteResponse(httpContext, includeDetails: true);

    public static Task WriteProductionResponse(HttpContext httpContext, Func<Task> next)
        => WriteResponse(httpContext, includeDetails: false);

    private static async Task WriteResponse(HttpContext httpContext, bool includeDetails)
    {
        // Try and retrieve the error from the ExceptionHandler middleware
        var exceptionDetails = httpContext.Features.Get<IExceptionHandlerFeature>();
        var ex = exceptionDetails?.Error;

        // Should always exist, but best to be safe!
        if (ex != null)
        {
            // Get the details to display, depending on whether we want to expose the raw exception
            var title = includeDetails ? "An error occurred: " + ex.Message : "An error occurred";
            var details = includeDetails ? ex.ToString() : null;

            var code = HttpStatusCode.InternalServerError; // 500 if unexpected

            if (ex is AccessDeniedException)
            {
                code = HttpStatusCode.Forbidden;
            }
            if (ex is AuthorizationException)
            {
                code = HttpStatusCode.Unauthorized;
            }
            else if (ex is NotFoundException)
            {
                code = HttpStatusCode.NotFound;
            }
            else if (ex is ApplicationLayerException)
            {
                code = HttpStatusCode.BadRequest;
            }

            var problem = new ProblemDetails
            {
                Status = (int?)code,
                Title = title,
                Detail = details
            };

            // This is often very handy information for tracing the specific request
            var traceId = Activity.Current?.Id ?? httpContext?.TraceIdentifier;
            if (traceId != null)
            {
                problem.Extensions["traceId"] = traceId;
            }

            httpContext.Response.ContentType = "application/problem+json";
            httpContext.Response.StatusCode = (int)code;

            //Serialize the problem details object to the Response as JSON (using System.Text.Json)
            var stream = httpContext.Response.Body;
            await JsonSerializer.SerializeAsync(stream, problem);
        }
    }
}

This is the configuration extension method we’ve declared in our DependencyInjectionExtensions class.


public static void UseCustomErrors(this IApplicationBuilder app, IHostEnvironment environment)
{
    if (environment.IsDevelopment())
    {
        app.Use(CustomErrorHandlerHelper.WriteDevelopmentResponse);
    }
    else
    {
        app.Use(CustomErrorHandlerHelper.WriteProductionResponse);
    }
}

This is how we wire it into the Startup class.


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // Approach 2: use ASP.NET Core ExceptionHandlerMiddleware.
    app.UseExceptionHandler(app2 => app2.UseCustomErrors(env));
...

When we again throw exceptions using the test controller, the results are what we’d expect.

Proper HTTP Response Codes Returned

Handling Error Responses in the UI

Now that we’ve figured out how to catch exceptions in the Web API and send them across the wire as a ProblemDetails instance with appropriate HTTP response code, let’s see what we can do to handle those on the Angular side.

In a nutshell, the primary means by which we handle error responses is by using HTTP interceptors. Think of these Angular’s version of middleware—they are essentially components which manipulate the HTTP request/response streams, and follow a chain of responsibility pattern, just like what ASP.NET Core does.

Here is the implementation of a custom HttpInterceptor that I created for the demo application.


@Injectable()
export class HttpErrorInterceptor implements HttpInterceptor {
    constructor(private injector: Injector) {}
    intercept(
        request: HttpRequest<any>,
        next: HttpHandler
    ): Observable<HttpEvent<any>> {
        return next.handle(request).pipe(
            catchError((response) => {
                if (
                    response instanceof HttpErrorResponse &&
                    response.error instanceof Blob &&
                    response.error.type === "application/problem+json"
                ) {
                    return new Promise<any>((resolve, reject) => {
                        const notificationService = this.injector.get(
                            NotificationService
                        );
                        const router = this.injector.get(Router);

                        if (response.status === 401) {
                            router.navigate(["unauthorized"]);
                        } else {
                            const reader = new FileReader();
                            reader.onload = (e: Event) => {
                                try {
                                    const error = JSON.parse(
                                        (<any>e.target).result
                                    );
                                    notificationService.error(error.title);
                                    reject(
                                        new HttpErrorResponse({
                                            error:
                                                error.title +
                                                " / " +
                                                error.detail,
                                            headers: response.headers,
                                            status: response.status,
                                            statusText: response.statusText,
                                            url: response.url
                                        })
                                    );
                                } catch (e) {
                                    reject(response);
                                }
                            };
                            reader.onerror = (e) => {
                                reject(response);
                            };
                            reader.readAsText(response.error);
                        }
                    });
                }

                return throwError(response);
            })
        );
    }
}

And here is how we wire it up inside the core module (core.module.ts).


    providers: [
        {
            provide: HTTP_INTERCEPTORS,
            useClass: HttpErrorInterceptor,
            multi: true
        },
...

Just a few quick points to make before we conclude.

We are taking advantage of Angular’s dependency injection mechanism to get a reference to the Notification service we got for free when we spliced in the ANMS starter kit.

We are also using dependency injection to get a hold of the Router, so that we can direct users to a "Unauthorized" page on 401 errors.

If possible, we try to display the custom error message that was set by our ASP.NET Core error handling middleware/function at the Web API side. While running in development, this will be the descriptive message of the exception that was thrown. When running in production, it will be a general error message.

As before, the expected HTTP response codes are being set in the response. However, now the UI is handling them by displaying a popup message, or redirecting to an error page, as you can see below.

ApplicationLayerException (400):

Application Layer Exception

NotFoundException (404):

Not Found Exception

InvalidOperationException (500):

Invalid Operation Exception

AuthorizationException (401):

Authorization Exception

So far so good. We have successfully bridged the gap between ASP.NET Core and Angular, thereby presenting a much more seamless user experience for our application.

Conclusion

In this blog entry I went over the high-level architecture of our Clean Domain-Driven Design web application. I presented again those diagrams from previous entries showing the layering of our system, and showed the direction of exception flow when something goes wrong in the app. I briefly gave my professional opinion on best practices for naming, throwing, and handling exceptions. I explained the impossibility of trying to account for all error conditions in controller methods (i.e. don’t throw any exceptions) on the one hand and throwing exceptions gratuitously (i.e. using exceptions for flow control) on the other. I presented my opinion that neither approach is right in the extreme, and that we should try to seek a middle ground. Then I went into different approaches for catching exceptions in the Web API using middleware, and how to serialize those exceptions across the wire using HTTP standards-compatible methodologies. Finally, I wrapped up by demonstrating how to respond to the error responses generated by the Web API in the Angular UI using HTTP interceptors, thus providing a better user experience.

Housekeeping Matters

I enjoy writing these blog entries and helping other developers, however I’m at a point right now where I need to prioritize my time in order to finish some other projects that I have on my plate. Therefore, I’m going to take a break from blogging for a few months in order to stay heads down and accomplish those goals. I may or may not write another blog entry here or there, time permitting, but for the most part you can expect radio silence from my end.

That being said, if you need to reach out to me, please do so! I’d be happy to answer any questions you have or discuss anything software development related. You can connect with me on LinkedIn, Twitter, Facebook, or Medium by clicking on the "connect/follow" links at the top of the main page of my blog, or you can contact me directly using the form at jacobsdata.com.

Along the way, I’ll make a few updates to the demo application on GitHub. Now that .NET 5 has finally launched, there are lots of great new tools and features to try to incorporate into the app.

Happy coding!

Experts / Authorities / Resources


Official Docs

Wikipedia

Andrew Lock

Robert C. Martin ("Uncle Bob")

Official Microsoft Docs


This is entry #4 in the Scaffold Your Clean DDD Web App Series

If you want to view or submit comments you must accept the cookie consent.