Cost of exceptions

Hopefully the era of leprosy and corona is over for this time and it’s time to get back to blogging. Exceptions are powerful feature of object-oriented languages as far as they are used like they are thought to use – throw exception only when something really unexpected happens. This advice should be taken seriously – here’s why.

Back in days when I was young and beautyful (now I’m only beautyful) I found utility application to get documents and their metadata out from SharePoint 2001. Although servers were powerful the exporting process was very slow. I took source code of utility and added bunch of sanity checks before exception handling to make sure that code doesn’t hit try-catch blocks if it can be avoided. Instead of 10 days our exports took 4.5 days after my little tweaks.

It’s hard to notice the effect of exceptions if we measure just one exception but they can be fatal to performance when they appear in loops. Let’s dig a little bit deeper.

How exceptions are handled?

Those who think exceptions are just fancy way to return errors are usually surprised when they hear how complex can things be internally – somewhere deep in Common Language Runtime (CLR).

The excellent book Expert .NET 2.0 IL Assembler by Serge Lidin describes what goes on under the hood.

The execution engine of the CLR processes an exception in two passes. The first pass determines which, if any, of the managed handlers will process the exception. Starting at the top of the Exception Handling (EH) table for the current method frame, the execution engine compares the address where the exception occured to the TryOffset and TryLength entries of each EH clause. If it finds that the exception happened in guarded block, the execution engine checks to see whether the handler specified in this clause will process the exception. … If none of the clauses in the EH table for the current method is suited to handling the exception, the execution engine steps up the call stack and starts checking the exception against EH tables of the method that called the method where the exception occured.

During the second pass, the finally and fault handlers are invoked with an empty evaluation stack. These handlers do nothing about the exception itself and work only with method arguments and local variables, so the execution engine doesn’t bother providing the exception object.

Without any numbers there’s are two alert reds for me:

  1. Processing in two phases
  2. Climbing up in method call stack and checking for exception handlers

These two activities both take probably more time than “usual” things we are doing in code.

Exception versus avoiding exception

Let’s get into code and numbers to get better understanding about exceptions effect to performance. I’m using simple program that fills list with some strings and nulls. After this let’s ask string length for each element in list and measure how long it takes.

Let’s start with version where asking string length is in try-catch block.

var list = new List<string>();
for (int i = 0; i < Math.Pow(10, 6); i++)
{
    if (i == 1 || i % 2 != 0)
    {
        list.Add(i.ToString());
    }
    else
    {
        list.Add(null);
    }
}

var watch = new Stopwatch();
watch.Start();

foreach (var s in list)
{
    try
    {
        var i = s.Length;
    }
    catch (Exception ex)
    {
        var e = ex.Message;
    }
}

watch.Stop();
Console.WriteLine(watch.Elapsed);

On my machine this code ran through with 4.58 seconds.

Let’s remove exception handling and replace it with null check so we don’t ask length of null-string.

var list = new List<string>();
for (int i = 0; i < Math.Pow(10, 6); i++)
{
    if (i == 1 || i % 2 != 0)
    {
        list.Add(i.ToString());
    }
    else
    {
        list.Add(null);
    }
}

var watch = new Stopwatch();
watch.Start();

foreach(var s in list)
{
    if(s == null)
    {
        continue;
    }

    var i = s.Length;
}

watch.Stop();
Console.WriteLine(watch.Elapsed);

After this modification the code takes 0.008 seconds to run. It’s roughly taken 570 times faster than letting code fall to exception. So, cost of exceptions can be very high.

Wrapping up

Exceptions are powerful feature and without exceptions we should think about our own mechanism how to organize error handling in our code. Like all other powerful features exceptions come with cost. This blog post demonstrated one aspect of it and one popular way how exceptions are abused. The code samples here went from 4.5 seconds to 0.008 seconds by avoiding exceptions. For long processes the win in time can be way bigger.

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    12 thoughts on “Cost of exceptions

    • June 28, 2021 at 9:42 pm
      Permalink

      Your samples demonstrate that an explicit safety check is far more performant than a code failing to an exception…
      Unfortunately, sometimes safety checks are insufficient due to lack of knowledge about possible exceptions.
      A further comparison would be valuable: how much is the cost of the try/catch block even with null check in place? This would help to evaluate how convenient is to write try/catch blocks.

    • June 28, 2021 at 10:05 pm
      Permalink

      Good article. I tried with Benchmark.NET and cost of try catch block is 400% more than a simple null check. And ofcourse we cannot have all possible exceptions sometimes. But still it matters 👏🏻

    • June 29, 2021 at 1:51 am
      Permalink

      Personal anecdotal rationale for why exceptions are horrible: they use reflection to establish their context when working out things like the callstack.

      I don’t know a great way to confirm this, but there are a variety of properties of System.Exception that imply some introspection of the throwing class has taken place (I believe the methodinfo is available from memory).

      As I say, anecdotal, but a major reason I avoid them where possible.

    • June 29, 2021 at 2:00 am
      Permalink

      Never pass or return null (use Option type if you need to represent ‘None’) and use monad results instead of exceptions for error handling and you will be a winner in life (and even better use programming languages that do no allow variables to be assigned to null.)

    • June 29, 2021 at 2:44 am
      Permalink

      What if exceptions are handled using Exception middleware in asp.net core?

    • June 29, 2021 at 5:34 am
      Permalink

      Agreed. I always try to avoid situations where an exception may occur, to take it a step further I believe if i have to use an exception handler I have already written bad code. Dart’s nnbd is quite a step in the right direction and i wish more languages start doing it.

    • June 29, 2021 at 5:51 am
      Permalink

      Maulik, ASP.NET Core exception middleware doesn’t solve anything if we are talking about performance. The price of throwing and catching exception will remain the same.

    • June 29, 2021 at 9:54 am
      Permalink

      I think the main point here, and I see it done quite often, is don’t use exceptions to test conditions. Quite often I see people write code that is *intended* to throw an exception on every pass through a method. I pretty much only use exceptions when something really has gone wrong, and I expect to return all the way up the callstack.

      I get that this defeats the purpose of a try/catch. But there is still use cases for it, just not when it’s intended that your exception handling should be used every single pass.

    • July 3, 2021 at 10:51 am
      Permalink

      Its worth reading. An eye opener of “when to use exception handler and when not to use it “. Thanks. Keep post things like this.

    • July 5, 2021 at 6:42 am
      Permalink

      Thanks for an interesting and thoughtful post. Like most things in writing code, throwing exceptions should be a matter of making a balanced judgement.

      Your code took a little over 4.5 seconds to throw half a million exceptions (about 0.00001 of a second each). There’s a trade off here. If I’m writing code that’s going to handle a very high volume of transactions I’ll pay careful attention to this, because performance is likely to be a key factor. In back end code I’d have it front of mind all the time.

      If I’m writing code where performance isn’t an issue (business logic in a WPF app that a user is interacting with, for example) then performance at this level probably isn’t a top priority. That doesn’t mean I don’t have an eye on it and don’t put in guards to prevent exceptions. It means I preference making my code well structured and readable code over saving a few milliseconds execution time.

    • July 5, 2021 at 11:11 am
      Permalink

      On .NET5 i get similar results as in this post.
      But on (old) .NET Framework (4.8) try/catch runs slighly faster as version with if.
      0.002 sec for try catch and 0.003sec for if

      Why so?

    • July 26, 2021 at 9:41 am
      Permalink

      What a nice article author.Thank you. Keep it up.

    Leave a Reply

    Your email address will not be published. Required fields are marked *