Try/finally with no catch block in C#

By FoxLearn 3/14/2025 3:05:32 AM   93
In C#, a try/finally block is particularly useful when you need to guarantee certain actions will always be executed at the end of a method, regardless of whether an exception was thrown.

This behavior can be critical in scenarios such as resource management, where you need to ensure resources are released, or logging, where you want to track method entry and exit times. The finally block will always execute, except in one particular case, which I will explain in the “Unhandled Exception” section.

Common Use Cases for Try/Finally:

  • Releasing resources: When working with resources like database connections, device connections, or semaphores, it’s important to release these resources at the end of a method, regardless of whether an exception occurs.
  • Logging: You might want to log a trace at both the start and end of a method, regardless of whether an exception occurs.

This article will explain a scenario where a try/finally block is used, discuss what happens when unhandled exceptions occur, and highlight what happens if an exception is thrown within the finally block.

Using Try/Finally

The following example demonstrates how to use a try/finally block in a method that needs to log when the method starts and ends and ensure that a device is properly disconnected, even if an exception occurs:

void ExecuteCommandOnDevice(string deviceId, string command)
{
    Logger.Trace($"Start {nameof(ExecuteCommandOnDevice)} with params: {nameof(deviceId)}={deviceId}");
    
    var device = new Device();
    bool locked = false;

    try
    {
        device.Lock();
        locked = true;
        
        Logger.Trace("Attempting to connect to the device");
        device.Connect();
        
        device.SendCommand(command);
    }
    finally
    {
        device.TryDisconnect();
        
        if (locked)
            device.Unlock();
        
        Logger.Trace($"End {nameof(ExecuteCommandOnDevice)}");
    }
}

In this example:

  1. The try block attempts to lock the device, connect, and send a command.
  2. The finally block ensures the device is disconnected and unlocked regardless of whether an exception is thrown.

Behavior When No Exception is Thrown

When no exception is thrown during the execution, the output from the finally block will show the method ending and the device being unlocked:

2025-02-17 08:45:30.6572 level=Trace message=Start ExecuteCommandOnDevice with params: deviceId=192.168.0.2
2025-02-17 08:45:30.6909 level=Trace message=Attempting to connect to the device
2025-02-17 08:45:30.6909 level=Trace message=Device connected
2025-02-17 08:45:30.6909 level=Trace message=Command executed: Beep
2025-02-17 08:45:30.6909 level=Trace message=Attempted to disconnect device.
2025-02-17 08:45:30.6909 level=Trace message=Device unlocked
2025-02-17 08:45:30.6909 level=Trace message=End ExecuteCommandOnDevice

Behavior When an Exception is Thrown

If an exception is thrown during the method execution, the finally block still runs, ensuring proper cleanup. Here’s what happens when an exception occurs:

2025-02-17 08:47:21.8781 level=Trace message=Start ExecuteCommandOnDevice with params: deviceId=192.168.0.2
2025-02-17 08:47:21.9111 level=Trace message=Attempting to connect to the device
2025-02-17 08:47:21.9111 level=Trace message=Device connected
2025-02-17 08:47:21.9111 level=Trace message=Command executed: ShowPrompt
2025-02-17 08:47:21.9134 level=Trace message=Attempted to disconnect device.
2025-02-17 08:47:21.9134 level=Trace message=Device unlocked
2025-02-17 08:47:21.9134 level=Trace message=End ExecuteCommandOnDevice
2025-02-17 08:47:21.9312 level=Error message=DeviceException: Command failed because the device is disconnected
   at Device.SendCommand(String command) in C:\path\to\Program.cs:line 78
   at Program.ExecuteCommandOnDevice(String deviceId, String command) in C:\path\to\Program.cs:line 42
   at Program.Main(String[] args) in C:\path\to\Program.cs:line 21

Even though an exception occurred, the finally block was executed, ensuring the device was properly disconnected and unlocked.

Unhandled Exceptions and the Finally Block

In the event of an unhandled exception, the finally block will still execute. However, it’s essential to understand how the unhandled exception can affect your program.

When there is no catch block to handle the exception, the program will crash, but the finally block will still run:

2025-02-17 08:48:57.6742 level=Trace message=Start ExecuteCommandOnDevice with params: deviceId=192.168.0.2
2025-02-17 08:48:57.7057 level=Trace message=Attempting to connect to the device
2025-02-17 08:48:57.7057 level=Trace message=Device connected
2025-02-17 08:48:57.7057 level=Trace message=Command executed: ShowPrompt
2025-02-17 08:48:58.5032 level=Trace message=Attempted to disconnect device.
2025-02-17 08:48:58.5032 level=Trace message=Device unlocked
2025-02-17 08:48:58.5032 level=Trace message=End ExecuteCommandOnDevice

Unhandled exception. DeviceException: Command failed to send because the device is disconnected
   at Device.SendCommand(String command) in C:\path\to\Program.cs:line 83
   at Program.ExecuteCommandOnDevice(String deviceId, String command) in C:\path\to\Program.cs:line 47
   at Program.Main(String[] args) in C:\path\to\Program.cs:line 19

If you wish to log unhandled exceptions before the program crashes, you can add an UnhandledException handler:

AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) =>
{
    Logger.Error(e.ExceptionObject.ToString());
};

Handling Exceptions in Finally Blocks

It is not recommended to throw exceptions from within the finally block, as doing so can obscure the original exception. If an exception is thrown in the finally block after one has been thrown in the try block, the exception in the finally block will often "hide" the original exception, making it harder to diagnose the root cause of the problem.

To avoid this, ensure that you handle exceptions in the finally block carefully, ideally without throwing new exceptions.

In summary, try/finally blocks in C# ensure cleanup operations are always executed, regardless of exceptions. However, handling unhandled exceptions, logging them, and being cautious about exceptions in the finally block are key aspects of managing errors effectively in your code.