Easy Modular Monolith — Part 4— Global Exception Handling

Norbert Dębosz
ITNEXT
Published in
4 min readSep 20, 2021

--

Global Exception Handling is the next feature that we will implement in our Modular application. It will allow us to handle all unpredicted and predicted exceptions in our system and deal with them — log them and return a user-friendly message to a client.

Architecture

Let’s establish some requirements.

  • We would like to have a single place that can catch exceptions from any of our modules.
  • We should be able to transform exceptions into user-friendly messages.

A single place to catch them all

The easiest way to get the context of any request in a .net core application is to use middleware. Every request to our application will need to visit middleware, and hence we will be able to act — in our case, log exceptions and prepare a user-friendly message that will be returned to a client.
It will be the “single place to catch them all”.
Let’s take a look:

Wrapping line 26 in the try/catch statement will guarantee that if any (almost) unhandled exception will be thrown anywhere later in the pipeline, we will catch it here.
The heart of functionality lies in the “catch” statement.

  • First, base on exception type, we resolve a logger level — all Domain and Application exceptions will be logged as “Information” as those are exceptions that we predicted could happen.
    If there is any other exception, then we log it as an error.
  • Next, we resolve a HttpStatusCode — for predicted exceptions return 400. For unpredicted 500.
  • Last but not least is creating a user-friendly message that will be returned to the client. Here is where our status code can be handy — base on it we can transform a

The last thing to do is write a response — it will be returned to the client.

Exceptions abstraction

The above middleware is stored in ModularMonolith.Infrastructure project which is part of our global infrastructure. It uses a common interface declared in ModularMonolith.Exceptions.Abstraction that provides abstract classes from which all exceptions in our system will inherit.
This will allow us to handle exceptions from each module in one place — the ExceptionLoggingMiddleware.

Idea base on splitting exceptions into two types:

  • Domain Exceptions — all exceptions from our domain layer (NameToShort, EmptyDescription etc.).
  • Application Exceptions — all exceptions from the application layer (NotFound, AlreadyExstis etc.).

Those two are base classes from which we will be able to create custom exceptions in our modules. Exceptions abstraction will be stored in the new project ModularMonolith.Exceptions.Abstraction.

Both classes contain an extra property called StatusCode — this property is used to create a user-friendly message in the middleware. Hence we have a single place that will handle our exceptions and prepare user-friendly messages.

Usage:

Let’s create a DomainException in the ProductModule.
To be able to use our DomainException class, we have to reference the ModularMonolith.Exceptions.Abstraction to ModularMonolith.Product.Domain project.

Product Module NameRequired Exception

And we can use the new class in the Product entity (line 23).

Here is the result when we request with an empty product name:

  • The exception has been logged by our logger.
  • Correct status code and message have been returned to the client.

Summary

Creating abstraction over exception handling makes our life much easier in terms of maintaining whole functionality. Instead of having multiple try/catches across modules, we move that responsibility to one place — our middleware.

The whole code is available here:

https://github.com/Ridikk12/ModularMonolith/tree/Logging

Previous:

In Next Part:

  • Direct communication between modules.

In future (this list can change):

  • Authentication/Authorization
  • OutBox improvements.
  • Domain events.
  • Unit/Integration tests.
  • Storing configuration.
  • Direct communication between modules.
  • The database approaches (multiple data sources).
  • Preparing for Microservices (Replacing MediatR by RabbitMq).
  • Migrate to Microservices.

References:

--

--

Solution Architect | Tech Lead | .Net Developer who searches for the perfect balance between business values and code quality.