Secret ingredients to quality software

SSW Foursquare

Rules to Better Error Handling - 7 Rules

We're all aware how painful it can be when you see a nasty error message. In the best case it makes you feel uneasy and at worst it leaves you blocked.

The user experience should not be an incredibly jarring jumble of text that looks like it is designed for a computer ūü§Ę. On the other hand, a developer should not be developing new features when they are oblivious to the unhealthy state of the application.

For developers, it is crucial to log errors, watch them daily and review the health of the errors in the Sprint Review.

Let's jump in and look at some of the best practices.

  1. When developing software, exceptions are a fact-of-life you will need to deal with. Don't reinvent the wheel, use an existing exception handling library or service.

    The best exception handling libraries are:

    • Application Insights (recommended)
    • Seq
    • RayGun

    Your users should never see the ‚Äúyellow screen of death‚ÄĚ in ASP.NET, or the ‚Äúunhandled exception‚ÄĚ message in a Windows application. Errors should always be caught and logged ‚Äď there are plenty of great services that help you fall into the pit of success. They show you great dashboards, integrate with your preferred communication tools, allow you to get great telemetry, and help you drill down to the root cause. As developers you should be alerted when something is going wrong and be able to see details to help you track down and fix bugs before clients notice them and call up asking you to fix it. With exception libraries, you should already be on it.

    default asp error 500 small
    Figure: Bad - If you see this, you are doing something wrong!

    timepro error
    Figure: Good - A nice custom error page

    Application Insights

    Application Insights is recommended whenever possible. If you are still developing Windows applications, then you can still use Application Insights, read Monitoring usage and performance in Classic Windows Desktop apps for more details.

    Application Insights will tell you if your application goes down or runs slowly under load. If there are any uncaught exceptions, you’ll be able to drill into the code to pinpoint the problem. You can also find out what your users are doing with the application so that you can tune it to their needs in each development cycle.

    Application Insights gives you very useful graphs and analysis which give you a good overview of how things are going. See Rules to Better Application Insights for more details.

    overview
    Figure: Good - Application Insights gives you graphs and analysis that help you find issues, but also lets you drill down to get the details as well

    If Application Insights is not available, we use Seq when developing web applications. Seq is great for identifying specific issues and how to fix them, but is not as good as Application Insights at letting you see the big picture.

    Seq

    Seq is built for modern structured logging with message templates. Rather than waste time and effort trying to extract data from plain-text logs with fragile log parsing, the properties associated with each log event are captured and sent to Seq in a clean JSON format. Message templates are supported natively by ASP.NET Core, Serilog, NLog, and many other libraries, so your application can use the best available diagnostic logging for your platform.

    xn4QHnmBS0Kx39gOv0wM GettingStarted 1
    Figure: Good - Seq provides you with plenty of details about what is happening, but if you don't already know what you're looking for, it can be tricky to parse

    RayGun

    Raygun is another great tool as it helps you identify and monitor errors in Single Page Applications.

    Figure: Good - Raygun gives you lots of information about errors and the "breadcrumbs" that led the user to the error in order to help you find issues

  2. Your users should never see the ‚Äúyellow screen of death‚ÄĚ. Errors should be caught, logged and a user-friendly screen displayed to the user.

    error screen bad
    Figure: Bad Example ‚Äď ASP.NET Yellow Screen of Death

    net core default
    Figure: Bad Example - Default exception page

    error screen good
    Figure: Good Example - GitHub custom error page

    However, as a developer you still want to be able to view the detail of the exception in your local development environment.

    How-to set up development environment exception pages in ASP.NET Core

    To set up exceptions in your local development environment you need to configure the Developer Exception Page middleware in the request processing pipeline.Unless you have modified the default template, it should work out of the box. Here are the important lines:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }
        ...
    }

    net core development
    Figure: This is how you set it up in .NET 5

    Find out more about exception handling in .NET Core 5 here.

  3. Do you use the best trace logging library?

    Did you know that writing your own logging infrastructure code wastes time? There are awesome logging abstractions in .NET Core and .NET 5+ that you should use instead!

    These abstractions allow you to:

    • Create log entries in a predictable and familiar fashion - you use the same patterns for logging in a Background Service as you would in a Blazor WASM app (just some slightly different bootstrapping ūüėČ)
    • Use Dependency Injection; your code doesn't take a dependency on a particular framework (as they are abstractions)
    • Filter output based off severity (Verbose/Debug/Info/Warning/Error) - so you can dial it up or down without changing code
    • Have different logs for different components of your application (e.g. a Customer Log and an Order Log)
    • Multiple logging sinks - where the logs are written to e.g. log file, database, table storage, or Application Insights
    • Supports log message templates allowing logging providers to implement semantic or structured logging
    • Can be used with a range of 3rd party logging providers

    Read more at Logging in .NET Core and ASP.NET Core

    trace logging bad
    Figure: Bad Example - Using Debug or Trace for logging, or writing hard coded mechanisms for logging does not allow you to configure logging at runtime

    trace logging bad 2
    Figure: Bad Example - Roll your own logging components lack functionality, and have not been tested as thoroughly for quality or performance as log4net

    _logger.LogInformation("Getting item {Id} at {RequestTime}", id, DateTime.Now);

    Good Example - Using templates allows persisting structured log data (DateTime is a complex object)

    seq2
    Figure: Good Example - Seq provides a powerful UI for searching and viewing your structured logs

  4. A good catch and re-throw will make life easier while debugging, a bad catch and re-throw will ruin the exception's stack trace and make debugging difficult.

    Catch and rethrow where you can usefully add more information that would save a developer having to work through all the layers to understand the problem.

    catch {} 
    
    catch (SomeException) {} 
    
    catch { throw; } 
    
    catch (SomeException) { throw; } 

    Bad Example - Never use an empty catch block. Do something in the block or remove it.

    catch (SomeException ex) { throw ex; } 
    
    catch (SomeException ex) { someMethod(); throw ex; } 

    Bad Example - Never re-throw exceptions by passing the original exception object. Wrap the exception or use throw; instead.

    catch (SomeException) 
    { 
         someMethod(); 
         throw; 
    }

    Good Example - Calling throw

    If you are following the Clean Architecture pattern - catching and rethrowing is useful for preventing your Infrastructure details from leaking into your Application e.g. we use a SQL server

    catch (SqlException ex) when (ex.Number == 2601)
    {
         throw new IdAlreadyTakenException(ex);
    }

    Good Example - By rethrowing a specific exception, my application code now doesn't need to know that there is a SQL database or the magic numbers that SQL exceptions use

  5. Do you catch exceptions precisely?

    In the try and catch block, if you always catch for normal Exception you will never know where the true problem is. When using try you should always expect some exception may happen, so in our code we always catch the specific exceptions.

    try 
    { 
         connection.Open();
    }
    catch (Exception ex) 
    { 
         return ex.ToString ();
    }

    Bad code ‚Äď Catching the general Exception

    try 
    { 
         connection.Open(); 
    }
    catch (InvalidOperationException ex) 
    { 
         return ex.ToString(); 
    }
    catch (SqlException ex) 
    { 
         return ex.ToString(); 
    }

    Good code - Catch with specific Exception

  6. While everyone knows that catch (Exception ex) is bad, no one has really noticed that throw new Exception() is worse.

    System.Exception is a very extensive class, and it is inherited by all other exception classes. If you throw an exception with the code throw new Exception(), what you need subsequently to handle the exception will be the infamous catch (Exception ex).

    As a standard, you should use an exception class with the name that best describes the exception's detail. All exception classes in .NET Framework follow this standard very well. As a result, when you see exceptions like FileNotFoundException or DivideByZeroException, you know what's happening just by looking at the exception's name. The .NET Framework has provided us a comprehensive list of exception classes that we can use. If you really can't find one that is suitable for the situation, then create your own exception class with the name that best describes the exception (e.g.: EmployeeListNotFoundException).

    Also, System.ApplicationException should be avoided as well unless it's an exception related to the application. While it's acceptable and should be used in certain cases, be aware that using it broadly will be just as bad as 'throw new Exception()'.

    public async Task<Unit> Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)
    {
            var entity = await _context.TodoLists.FindAsync(request.Id);
    
            if (entity == null)
            {
                    throw new Exception($"Couldn't find a todo list with id: {request.Id}");
            }
    
            ...
    }

    Figure: Bad example - System.Exception is thrown, you now need to read the code to try to work out what is going wrong (hard if it was thrown by code outside of this solution)

    public async Task<Unit> Handle(UpdateTodoListCommand request, CancellationToken cancellationToken)
    {
            var entity = await _context.TodoLists.FindAsync(request.Id);
    
            if (entity == null)
            {
                    throw new NotFoundException(nameof(TodoList), request.Id);
            }
    
            ...
    }
    ...
    public class NotFoundException : Exception
    {
            public NotFoundException()
                : base()
            {
            }
    
            public NotFoundException(string message)
                : base(message)
            {
            }
    
            public NotFoundException(string message, Exception innerException)
                : base(message, innerException)
            {
            }
    
            public NotFoundException(string name, object key)
                : base($"Entity \"{name}\" ({key}) was not found.")
            {
            }
        }

    Figure: Good example - A specific exception is thrown which you can specifically catch, the message is consistently formatted and a consuming application can understand what was wrong with their request easily

  7. The ability to see the overall health (performance counters, exceptions, data usages, page hit counts etc.) of your application ensures you are well in control of it and have all the necessary information at your hands to action any bugs or performance issues. An analytics framework allows you to do all of that in a consistent and centralised manner. 

    An analytics framework puts you in control of your application and allows you to do the following:

    • Capture,¬†log and action¬†exceptions
    • Analyse performance issues and identify¬†bottlenecks
    • Track application usage down to individual components
    • View and¬†create performance reports
    • Analyse¬†user¬†demographics

    There are a number of existing Analytics frameworks available on the market, so there is no need to "re-invent the wheel". Why would you write your own if someone else has already taken the trouble to do it? We recommend using one of these frameworks or services:

    Each one of those frameworks has a fairly extensive set of tools available and are easy to integrate into your application.

We open source. Powered by GitHub