SSW Foursquare

Rules to Better Error Handling - 8 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 application in the Sprint Review.

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

  1. Do you use the best exception handling library?

    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 example - If you see this, you are doing something wrong!

    timepro error
    Figure: Good example - 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.

    It 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 example - 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 example - 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 example - 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. Do you present the user with a nice error screen?

    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. Do you catch and re-throw exceptions properly?

    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.

    Using throw ex resets the stack trace, obscuring the original the error and may hide highly valuable information to debug this exception.

    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 a try-catch block, avoid catching generic Exception types as this masks the underlying problem. Instead, target only the specific exceptions you can manage, which helps in accurately identifying and rectifying the error.

    It is essential to foresee the exceptions that the code in the try block might raise. Catching these specific exceptions at the point of occurrence provides the most context for effectively addressing the issue.

    try 
    { 
         connection.Open();
    }
    catch (Exception ex) 
    { 
         // Omitted for brevity
    }

    Bad code ‚Äď Catching the general Exception

    try 
    { 
         connection.Open(); 
    }
    catch (InvalidOperationException ex) 
    { 
         // Omitted for brevity
    }
    catch (SqlException ex) 
    { 
         // Omitted for brevity
    }

    Good code - Catch with specific Exception

    To further elaborate, here are some reasons why catching specific exceptions is important:

    1. Contextual Handling - Specific exceptions enable tailored responses. You can close resources in response to an IOException or take other actions for a NullPointerException.
    2. Code Readability - Specific exceptions make code more readable. They allow developers to better anticipate potential errors, making the code easier to maintain.
    3. Debugging and Traceability - A detailed exception type speeds up debugging. A general exception conceals the root cause and complicates diagnosis.
    4. Logging - Catching a specific exception enables detailed logging, crucial for post-mortem analysis.
    5. Forward Compatibility - Specific exceptions minimize the risk of future updates causing unintended issues. A broad Exception class could inadvertently catch new, unrelated exceptions.
    6. Error Recovery - Knowing the exact type of exception informs whether to retry an operation, failover, or terminate the program.
    7. Resource Optimization - Catching broad exceptions is computationally expensive. Targeting specific exceptions allows for more optimized code.

    Global exception handlers for a program are an exception to the rule, as they need to catch any uncaught exceptions for the sake of good user experience. Frameworks often provide mechanisms for this scenario, such as:

  6. Do you know that you should never throw an exception using System.Exception?

    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. Do you use an analytics framework to help manage exceptions?

    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.

  8. Do you know how to manage errors with Code Auditor

    Code auditing is an essential practice that empowers developers, quality assurance teams, and organizations to identify and rectify potential flaws, weaknesses, and security risks within their codebase.

    SSW Code Auditor is the perfect tool to audit your code helping you find:

    • Broken links
    • HTML errors
    • Google Lighthouse issues

    Every Sprint, some time should be devoted to resolving Code Auditor errors and warnings. To aid in this, long-lasting PBI items should be created and carried over each Sprint keeping a history of the work done.

    The PBI should contain a version number at the top which gets incremented by +1 every new Sprint following the "Change x to y" rule. This is used to track how many Sprints the PBI has been active for.

    screenshot 2023 06 12 at 07 45 16
    Figure: PBI to track Code Auditor errors

We open source. Powered by GitHub