Saturday, 27 October 2018

C# Exception Handling: Catching, Throwing, and Performance

TL;DR;

  • Use `throw;` not `throw ex` 
  • Use `catch(...)` as few times as possible 
  • Reserve Exceptions for Exceptional conditions: 
    • 1 in 100 is not exceptional 
    • 1 in 1,000 probably isn't exceptional either

The Stack Trace

The Stack Trace is extremely useful for debugging errors, and can significantly shorten the cycle time of your work. There is however a common mistake that is made across many codebases that makes it less useful than it could be.

Let's start with an example...

A New Support Request

Your support team has identified an issue in production, they've diligently compiled:
  • The user affected
  • The time it occurred
  • A bunch of other useful information 
  • The actual error message 
  • The Stack Trace
Your stack trace looks like this:
System.Exception: I wasn't able to complete that request!
  at <Library>.<Service>.DoWork() in /Library/Service.cs:line21
  at <Library>.<Controller>.UserRequestedAction() in /Library/Controller.cs:line 21
You feel confident you can fix this quickly, after all, you have the actual error message and stack trace! Then you open up the code at the top of the stack trace - Service.cs:
    public void DoWork()
    {
        try        
        {
            var service = new ExceptionThrowingService();
            service.ErroneousMethod();
        }
        catch (Exception e)
        {
            // Perform special handling, logging, etc.            
            throw e; // Line 21!        
        }
    }

Uh oh, that isn't where the actual error message occurred!

In the sample code we can probably deduce that the error occurred in either the constructor of the service, or the call to ErroneousMethod(). You may have been more fortunate than I have, but rarely does the code I inherit look as simple as this. A Try/Catch block like this might have as little as 5 lines, but is often somewhere between 50-200 lines, and unfortunately, sometimes 1,000 lines or more.

In addition, the dependencies that are utilised (such as ExceptionThrowingService in this instance) mean that the actual error could have occurred somewhere within one of those dependencies, and therefore the number of lines of code to search for the error is significantly larger than just the lines within this Try/Catch block.

Maintaining the Stack Trace

Fortunately, maintaining the stack trace is easy. Replace throw e; with throw;. Simply omitting the Exception object will result in .Net not touching the stack trace.

Stack Trace Recommendation

Ensure that your default coding style is to use  throw; without including the Exception object. You can then selectively choose when you want to rewrite the Stack Trace.

It should be extremely rare that you want to overwrite the Stack Trace on the Exception object that you caught. If you are going to overwrite the Stack Trace, then create a new Exception. You may or may not want to include the caught exception as the Inner Exception depending upon your context.

Performance

While Exceptions are a powerful tool, they come at a cost, one of these costs is execution time. If you throw one Exception, you are unlikely to notice the cost, but if you throw thousands, or millions, then you are most likely paying a performance penalty for your choices.

Personal Anecdote: I have improved performance from hours to a couple of minutes by changing control flow from Exception throwing to alternative control flow such as return values. I have experienced this saving on multiple occasions.

The Stats

Using a skeleton implementation we can compare the execution time of a loop with exceptions vs a loop that doesn't throw exceptions. On a reasonably powerful modern laptop the timings come out at:
IterationsWith Exceptions (ms)Without Exceptions (ms)
1003< 1
1,00025< 1
10,000282< 1
100,0003,3531
1,000,00040,3059

This aligns with my personal experience, loops that perform calculations and/or validations across a large number of objects/records can often improve performance significantly be decreasing reliance on Exceptions.

These results are only going to be indicative when then execution time of the "work" being performed for each item in the loop is limited. This is often true when there is no reliance on a data store or network dependent services inside of the loop.

Performance Recommendation

Exceptions inflict a significant performance penalty whenever they are thrown. Exceptions should be reserved for exceptional circumstances. If 50% of the calls to a method result in an Exception being thrown, then this is not an exceptional situation, this is business as usual.

I appreciate that if the data is invalid, then from a theoretical point of view, it may be correct that an exception be thrown. However, if you want the services that you write to scale and deliver responsive user experiences, then consider the performance impact of all Exceptions that you throw and use them sparingly.

Example Code

Please see the example on BitBucket for a minimalist demonstration of these concepts.

References:

No comments:

Post a Comment