Stop using Exceptions for Flow Control

Aug 02, 2018 - 2 min read

From time to time, you might come across code that throws or catches exceptions that could have been avoided. Below are two simplified examples that demonstrate good and bad practice of writing code:

// Exception for flow control example - BAD
public bool IsValidAbsoluteAddress(string address)
{
    try
    {
        var uri = new Uri(address, UriKind.Absolute); // throws UriFormatException if URI is invalid or not absolute
    }
    catch (UriFormatException ex)
    {
        return false;
    }

    return true;
}
// Regular example - GOOD
public bool IsValidAbsoluteAddress(string address)
{
    return Uri.IsWellFormedUriString(address, UriKind.Absolute); // No exception thrown
}

Both of these methods basically do the same thing, yet the first one is significantly slower.

Exceptions are slow

When you throw an exception, the .NET runtime takes a snapshot of the thread state and stack trace, and then unwinds the stack until it encounters a matching catch block.

Source: http://mdfarragher.com/2017/11/08/the-exception-penalty-in-csharp/

In the first example, we have written a method that uses exceptions for flow control. So how should it be done?

Guidelines

When to catch{}

  • When you can handle and resolve the error
  • When you want to log the exception (and then rethrow it)

What to catch{}

  • File system exceptions
  • Timeouts
  • Invalid state

Generally, things your application doesn’t have control of.

When to throw

  • NULL, undefined, empty, NaN where a value is expected
  • Invalid parameter or string format
  • Business logic not satisfied

Generally, when an error occurs that cannot be fixed within the scope of the executing code or layer.

Best practices

  • Validate before you execute - return false; is always cheaper than throw new NullReferenceException();
  • Don’t put too much code inside a catch block - only code that can throw, and can be handled
  • Log your exceptions