Exceptions, catch or not?

Before we dive in, let’s define what does it mean to catch an exception. Many, if not all, programming languages have notion of an exception that terminates program execution flow; and most of languages have mechanism to handle exceptions, for example try-catch-finally language construct. Assuming familiarity with try-catch-finally we are going to focus on when to use it. When we say “catch an exception” we mean surrounding a block of code with try and catching exceptions that may occur in the surrounded block of code.

try
{
    // Do something.
}
catch (Exception ex)
{
    // Handle exception here.
}
finally
{
    // Code that executes no matter what.
}

When should I catch?

Luckily the question whether to catch or not is not so hard to answer. There is just one recommendation and it boils down to a simple question. Would it serve any specific purpose or add any value if we catch exceptions on a block of code? If the answer is “no”, then there is no need to catch. If the answer is “yes” or “maybe” then to help us decide let’s look at the list of reasons that indicates whether catching an exception makes sense.

Reasons:

  1. We need to handle specific type of exception and execute certain logic. For example, we would like to catch HTTP related exceptions and if code is 503 (“Service Unavailable”) we want to wait a little and retry.
  2. We need to handle certain exceptions and wrap it with our custom exception type to provide more troubleshooting information or help our error handling logic to properly address the exception at higher levels.
  3. We would like to log exceptions with additional information and then re-throw.
  4. We would like to silent exceptions for a block of code. Use this with extra caution! There must be a very valid reason to do so, because if used unthoughtfully it will hide potential problems in the code and make troubleshooting extremely hard. If still decided to proceed, make sure at least to log an exception.

Let’s look at each reason individually to better understand what each means.

1. Execute logic on exception

So, what that logic might be? Honestly, it may vary, but most common is to perform retry, release resources or revert state.

Retry

Code interactions withing a process are much less impacted by external factors than code that reaches outside of a process. Compare two scenarios

  1. Call a function that sums two integers. <<CODE>>
  2. Make an HTTP call to a service that sums two integers. <<CODE>>

Which scenario has higher likelihood to fail? Hope the answer is obviously the second scenario, due of added factor of network communication. Because network communications are far less reliable than in-process communications and we still want our code to be resilient, we apply difference defense mechanisms one of which is re-try logic. Re-try boils down to three steps: try to execute code that might fail, catch specific exception and try again.

Let’s take a look at example when we handle HTTP exception with status code 503 (service unavailable) and retry few times. On any other exceptions or HTTP exception with different status code we will not attempt to retry.

async Task<HttpResponseMessage> HandleHttpException()
{
    const int maxRetries = 2;
    int retries = 0;
    HttpClient client = new HttpClient();

    while (true)
    {
        try
        {
            return await client.GetAsync("https://programhappy.net");
        }
        catch (HttpRequestException httpRqException) when (httpRqException.StatusCode == HttpStatusCode.ServiceUnavailable)
        {
            if (retries <= maxRetries)
            {
                retries++;
            }
            else
            {
                throw;
            }
        }
    }
}

Release

After acquiring a resource, for example database connection, we must ensure it is released when not needed any more. We also need to take in account that exceptions break normal execution flow, therefore if not taken care of properly a resource may “leak”. The most trivial approach is to do try-catch and relace a resource after it is not needed or when exception happens.

void ReleaseResourceWithTryCatch()
{
    var connection = new SqlConnection("connection string here");
    try
    {
        connection.Open();

        // Do some logic here

        connection.Close();
    }
    catch (Exception)
    {
        connection.Close();
        throw;
    }
}

If programming language of your choice supports finally block, prefer it instead of the above.

void ReleaseResourceWithFinally()
{
    var connection = new SqlConnection("connection string here");
    try
    {
        connection.Open();

        // Do some logic here
    }
    finally
    {
        connection.Close();
    }
}

Some programming languages provide additional syntax for working with resources. Prefer to use it instead of try-catch-finally where it makes sense.

void ReleaseResourceWithUsing()
{
    using (var connection = new SqlConnection("connection string here"))
    {
        connection.Open();

        // Do some logic here
    }
}

Revert

It is quite common to implement logic that keeps program state valid at all the times. However not all state transitions are atomic or happen all at once, therefore if exception happens in the middle of state transition we need to revert back to previous state. Good example is writes to a database. For that reason, databases support transactions that allow to revert to previous state if something went wrong. We can use try-catch-finally to handle exception and revert transaction back if exception is thrown.

void RevertOperationWithTryCatch()
{
    using (var connection = new SqlConnection("connection string here"))
    {
        connection.Open();
        var transaction = connection.BeginTransaction("transaction name here");
        try
        {
            // Do some write here

            transaction.Commit();
        }
        catch (Exception)
        {
            transaction.Rollback();
            throw;
        }
    }
}

If programming language of your choice provides additional syntax, use it instead of try-catch.

void RevertOperationWithUsing()
{
    using (var connection = new SqlConnection("connection string here"))
    {
        connection.Open();
        using (var transaction = connection.BeginTransaction("transaction name here"))
        {
            // Do some write here

            transaction.Commit();
        }
    }
}

2. Custom exceptions

When creating a library, it is common to wrap exceptions that occur inside the library with custom library exceptions. This has multiple advantages:

  • Hide (incapsulate) library internal implementation and exceptions so that client does not depend or rely on library internals that may change at any time.
  • Better communicate library’s exception categories back to client to simplify its error handling logic.
  • Provide meaningful error messages that are relevant to the library’s context. 

Custom exception can also be used in application code to make error handling logic simplier. One important rule while wrapping exceptions is to provide original exception along with custom exception, hence help with troubleshooting. In the code below we catch SqlException and wrap it with custom MyCustomException.

public class MyCustomException : Exception
{
    public MyCustomException()
    { }

    public MyCustomException(string message)
        : base(message)
    { }

    public MyCustomException(string message, Exception innerException)
        : base(message, innerException)
    { }
}

...

void CustomExceptions()
{
    try
    {
        // Do something that may throw SqlException.
    }
    catch (SqlException sqlEx)
    {
        throw new MyCustomException("Error while doing so and such.", sqlEx);
    }
}

3. Logging exceptions

Logging is another quite common reason to catch exceptions. There are few important details to keep in mind:

  • Make sure that logs include additional useful information beside an exception itself.
  • When logging an exception, it’s important to log entire stack trace. Not all programming languages will include stack trace when casting exception to string, therefore look for language specific approach. It could be an additional parameter to logger methods to indicate that exception should be logged in full details. Code example below uses ILogget<T>.LogError(Exception ex, string message) method to ensure entire exception is logged.
  • If we do just logging and would like to propagate exception further as is we need to re-throw it; and of course it’s language specific syntax.
void LoggingException()
{
    try
    {
        // Do something that may throw SqlException.
    }
    catch (SqlException sqlEx)
    {
        _logger.LogError(sqlEx, "Error while doing so and such.");
        throw;
    }
}

Be aware that even most healthy food in large quantities is a poison, so as excessive try-catch-log-rethrow usage comes with drawbacks.

  • Logging is not free and slows down an application no matter what logging library is used.
  • Using try-catch-log-rethrow excessively on every level of code generates large amout of logs with repetitive errors that makes it hard to navigate through.
  • Code that was supposed to read well is polluted with try-catch statements making it hard to comprehend.

General advice is to try limit usage of try-catch-log-rethrow to:

  • Request handler functions for services.
  • User action handlers for UI application.
  • Cross-process interactions, for example wrapping code that makes a request to other service, sends a message or writes to a database and etc. It is useful to log requests and responses along with full exception stack trace.

4. Silencing exceptions

The technique of catching and silencing all or some exceptions is quite controversial and can backfire, because it doesn’t help to solve an issue but rather hides it. When using this technique, ensure that benefits you are getting overweight all negative consequences of hiding errors. However, if you decided that it is the right thing to do, at least ensure that captured exceptions are logged with full details. Below is an example of dangerously silencing all exceptions.

void SilentAllExceptions()
{
    try
    {
        // Do something that may throw an Exception
    }
    catch (Exception)
    {
        // Do nothing
    }
}

There is one place though where it is justifiable to stop further propagation of exceptions and prevent program crash. And it is right on the edge of a UI that interacts with users or the edge of a service or microservice. In this case exceptions are captured and

  • UI application usually logs an exception and displays a user-friendly error message. Console application may print error message and exit with non-zero error code.
  • Service or microservice logs an exception and returns error response.

Conclusion

If summarizing in one sentence, catch exceptions only when you plan to do something meaningful about it. Excessive try-catch blocks “pollute” code and make it hard to read and add no value at all.

Thank you for working through the post! If you have any questions or see opportunity to improve the material, please let me know. Looking forward to hear from you!

Posts created 20

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Related Posts

Begin typing your search term above and press enter to search. Press ESC to cancel.

Back To Top