C# – Try/finally with no catch block

Try/finally blocks are useful for when you are required to do something at the end of a method no matter what. The finally block always executes, even if there’s an exception (there is one case where this isn’t true, which I’ll explain in the Unhandled exception section below).

There are a few common scenarios where you’d typically want to use a try/finally block:

  • You’re using a resource and need to unconditionally release it at the end of the method. The resource could be a database connection, a connection to a device, a semaphore, or really anything that has to be cleaned up.
  • You need to log a trace message at the beginning and end of method calls.

In this article, I’ll show an example of code that uses a try/finally. Then I’ll explain what happens when unhandled exceptions are involved, and what happens when the finally block itself throws an exception.

Example of using try/finally

The following code is using a try/finally to meet two requirements:

  • It needs to log the start and end of the method.
  • It needs to disconnect from a device and release it.

Note: Assume TryDisconnect() and Unlock() don’t throw exceptions.

void SendCommandToDevice(string deviceId, string command)
{
	Logger.Trace($"Start {nameof(SendCommandToDevice)} with params: {nameof(deviceId)}={deviceId}");
	
	var device = new Device();
	bool locked = false;
	
	try
	{
		device.Lock();
		locked = true;
		
		Logger.Trace("Attempting to connect");
		device.Connect();
		
		device.SendCommand(command);
	}
	finally
	{
		device.TryDisconnect();
		
		if (locked)
			device.Unlock();
		
		Logger.Trace($"End {nameof(SendCommandToDevice)}");
	}
}
Code language: C# (cs)

Note: The code calling SendCommandToDevice() has a try/catch block. I’ll show an unhandled exception scenario in the section below.

Here’s what happens when no exception is thrown. Output from the finally block is highlighted:

2021-05-17 07:45:30.6572 level=Trace message=Start SendCommandToDevice with params: deviceId=192.168.0.2
2021-05-17 07:45:30.6909 level=Trace message=Locked device for exclusive use
2021-05-17 07:45:30.6909 level=Trace message=Attempting to connect
2021-05-17 07:45:30.6909 level=Trace message=Connected to device
2021-05-17 07:45:30.6909 level=Trace message=Attempting to send command Beep
2021-05-17 07:45:30.6909 level=Trace message=Attempted to disconnect device. It may have been disconnected already, and there's no side effects here
2021-05-17 07:45:30.6909 level=Trace message=Unlocked device
2021-05-17 07:45:30.6909 level=Trace message=End SendCommandToDevice
Code language: plaintext (plaintext)

And here’s what happens when an exception is thrown. Notice that the finally block is executed and the exception is logged after the method call:

2021-05-17 07:46:21.8781 level=Trace message=Start SendCommandToDevice with params: deviceId=192.168.0.2
2021-05-17 07:46:21.9111 level=Trace message=Locked device for exclusive use
2021-05-17 07:46:21.9111 level=Trace message=Attempting to connect
2021-05-17 07:46:21.9111 level=Trace message=Connected to device
2021-05-17 07:46:21.9111 level=Trace message=Attempting to send command ShowPrompt
2021-05-17 07:46:21.9134 level=Trace message=Attempted to disconnect device. It may have been disconnected already, and there's no side effects here
2021-05-17 07:46:21.9134 level=Trace message=Unlocked device
2021-05-17 07:46:21.9134 level=Trace message=End SendCommandToDevice
2021-05-17 07:46:21.9312 level=Error message=ErrorHandling.DeviceException: Command failed to send because the device is disconnected
   at ErrorHandling.Device.SendCommand(String command) in C:\makolyte\Program.cs:line 78
   at ErrorHandling.Program.SendCommandToDevice(String deviceId, String command) in C:\makolyte\Program.cs:line 42
   at ErrorHandling.Program.Main(String[] args) in C:\makolyte\Program.cs:line 21

Code language: plaintext (plaintext)

Unhandled exceptions and the finally block

When you have no catch block anywhere in the call stack, you’ll have an unhandled exception. In the previous section, I was calling SendCommandToDevice() from a try/catch, so the exception wasn’t unhandled.

I’ll remove the try/catch so that there’s an unhandled exception coming from SendCommandToDevice().

First, the finally block is executed, as expected:

2021-05-17 07:48:57.6742 level=Trace message=Start SendCommandToDevice with params: deviceId=192.168.0.2
2021-05-17 07:48:57.7057 level=Trace message=Locked device for exclusive use
2021-05-17 07:48:57.7057 level=Trace message=Attempting to connect
2021-05-17 07:48:57.7057 level=Trace message=Connected to device
2021-05-17 07:48:57.7057 level=Trace message=Attempting to send command ShowPrompt
2021-05-17 07:48:58.5032 level=Trace message=Attempted to disconnect device. It may have been disconnected already, and there's no side effects here
2021-05-17 07:48:58.5032 level=Trace message=Unlocked device
2021-05-17 07:48:58.5032 level=Trace message=End SendCommandToDevice

Code language: plaintext (plaintext)

Second, the unhandled exception crashes the program. I’m using a Console App, so the unhandled exception is written to the console by the system, like this:

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

Notice that the log has no indication that there was an error. The exception is not logged. It is written to the console and will appear in the Windows Event Log, but ideally it’d be nice if this exception were logged with everything else.

UnhandledException handler

What if you want to log the unhandled exception before the program crashes? You can do that by wiring up an UnhandledException handler, like this:

AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) =>
{
	Logger.Error(e.ExceptionObject.ToString());
};
Code language: C# (cs)

Here’s what happens with an unhandled exception when an UnhandledException handler is involved:

2021-05-17 08:09:05.5107 level=Trace message=Start SendCommandToDevice with params: deviceId=192.168.0.2
2021-05-17 08:09:05.5456 level=Trace message=Locked device for exclusive use
2021-05-17 08:09:05.5456 level=Trace message=Attempting to connect
2021-05-17 08:09:05.5456 level=Trace message=Connected to device
2021-05-17 08:09:05.5456 level=Trace message=Attempting to send command ShowPrompt
2021-05-17 08:09:05.5706 level=Error message=ErrorHandling.DeviceException: Command failed to send because the device is disconnected
   at ErrorHandling.Device.SendCommand(String command) in C:\makolyte\Program.cs:line 83
   at ErrorHandling.Program.SendCommandToDevice(String deviceId, String command) in C:\makolyte\Program.cs:line 47
   at ErrorHandling.Program.Main(String[] args) in C:\makolyte\Program.cs:line 19
2021-05-17 08:09:06.3830 level=Trace message=Attempted to disconnect device. It may have been disconnected already, and there's no side effects here
2021-05-17 08:09:06.3830 level=Trace message=Unlocked device
2021-05-17 08:09:06.3830 level=Trace message=End SendCommandToDevice

Code language: plaintext (plaintext)

First, notice where the exception showed up? It is logged before the messages from the finally block. This reveals an interesting fact about exceptions and the finally block. The exception is caught first (in this case, by an UnhandledException handler), then execution is routed back to the finally block.

Does the finally block always execute? No

When you put Environment.Exit() in your UnhandledException handler, then your finally block won’t run.

Sometimes you’ll see examples of the UnhandledException handler with Environment.Exit(), like this:

AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) =>
{
	Logger.Error(e.ExceptionObject.ToString());
	Environment.Exit(1);
};
Code language: C# (cs)

Here’s what happens in this scenario:

2021-05-17 08:14:50.5597 level=Trace message=Start SendCommandToDevice with params: deviceId=192.168.0.2
2021-05-17 08:14:50.5915 level=Trace message=Locked device for exclusive use
2021-05-17 08:14:50.5915 level=Trace message=Attempting to connect
2021-05-17 08:14:50.5915 level=Trace message=Connected to device
2021-05-17 08:14:50.5915 level=Trace message=Attempting to send command ShowPrompt
2021-05-17 08:14:50.6101 level=Error message=ErrorHandling.DeviceException: Command failed to send because the device is disconnected
   at ErrorHandling.Device.SendCommand(String command) in C:\makolyte\Program.cs:line 83
   at ErrorHandling.Program.SendCommandToDevice(String deviceId, String command) in C:\makolyte\Program.cs:line 47
   at ErrorHandling.Program.Main(String[] args) in C:\makolyte\Program.cs:line 19
Code language: plaintext (plaintext)

Notice the finally block didn’t execute?

Yeah, don’t call Environment.Exit() in your UnhandledException handler unless you are intentionally trying to stop the finally block from running.

An exception in a finally block

If you have an exception in a finally block, and no exception in the try block, then the exception will be unhandled and crash the program, even if there is a try/catch somewhere.

But what happens when an exception is thrown from the try block, and then an exception is thrown from the finally block? It depends on if the original exception is unhandled or not.

In any case, do everything you can to avoid throwing exceptions in the finally block.

If you have a try/catch, the exception from the finally block will actually hide the original exception

Let’s say I’m calling SendCommandToDevice() from a try/catch. In SendCommandToDevice(), one of the device calls is throwing a DeviceException. Then in the finally block, device.Unlock() throws an ArgumentException:

//Calling SendCommandToDevice
try
{
	SendCommandToDevice("192.168.0.2", "ShowPrompt");
}
catch (Exception ex)
{
	Logger.Error(ex.ToString());
}


//SendCommandToDevice finally block
device.TryDisconnect();
device.Unlock(); //Throws ArgumentException
Logger.Trace($"End {nameof(SendCommandToDevice)}");
Code language: C# (cs)

Here’s what will happen:

2021-05-17 08:35:16.1968 level=Trace message=Start SendCommandToDevice with params: deviceId=192.168.0.2
2021-05-17 08:35:16.2291 level=Trace message=Locked device for exclusive use
2021-05-17 08:35:16.2291 level=Trace message=Attempting to connect
2021-05-17 08:35:16.2291 level=Trace message=Connected to device
2021-05-17 08:35:16.2291 level=Trace message=Attempting to send command ShowPrompt
2021-05-17 08:35:16.2291 level=Trace message=Attempted to disconnect device. It may have been disconnected already, and there's no side effects here
2021-05-17 08:35:16.2490 level=Error message=System.ArgumentException: Value does not fall within the expected range.
   at ErrorHandling.Device.Unlock() in C:\makolyte\Program.cs:line 82
   at ErrorHandling.Program.SendCommandToDevice(String deviceId, String command) in C:\makolyte\Program.cs:line 49
   at ErrorHandling.Program.Main(String[] args) in C:\makolyte\Program.cs:line 23
Code language: plaintext (plaintext)

Notice that it’s logging the ArgumentException and not the DeviceException? This is because exceptions thrown from the finally block hide the original exception. This makes troubleshooting problems really difficult, because the actual problem is lost.

If you have an unhandled exception, then the exception from the finally block will have no impact

This time let’s say there’s no try/catch, so exceptions coming out of SendCommandToDevice() will be unhandled. Just like the scenario above, let’s say a device method in SendCommandToDevice() throws a DeviceException, and then the finally block also throws an ArgumentException.

In this scenario, the unhandled DeviceException is logged and the ArgumentException from the finally block is completely lost:

2021-05-17 08:40:55.7396 level=Trace message=Start SendCommandToDevice with params: deviceId=192.168.0.2
2021-05-17 08:40:55.7760 level=Trace message=Locked device for exclusive use
2021-05-17 08:40:55.7760 level=Trace message=Attempting to connect
2021-05-17 08:40:55.7760 level=Trace message=Connected to device
2021-05-17 08:40:55.7760 level=Trace message=Attempting to send command ShowPrompt
2021-05-17 08:40:55.7962 level=Error message=ErrorHandling.DeviceException: Command failed to send because the device is disconnected
   at ErrorHandling.Device.SendCommand(String command) in C:\makolyte\Program.cs:line 75
   at ErrorHandling.Program.SendCommandToDevice(String deviceId, String command) in C:\makolyte\Program.cs:line 40
   at ErrorHandling.Program.Main(String[] args) in C:\makolyte\Program.cs:line 15
2021-05-17 08:40:56.6444 level=Trace message=Attempted to disconnect device. It may have been disconnected already, and there's no side effects here

Code language: plaintext (plaintext)

This is actually a far better outcome than losing the original exception. It’s still bad – because the finally block is not fully executing (due to it throwing an exception), but at least you don’t lose information about the original exception.

If there’s no exception in the try block, and there’s an exception in the finally block, it will be an unhandled exception

I’m executing SendCommandToDevice() in a try/catch block, so you would think any exceptions coming out of it would be caught. But that’s not the case when an exception comes out of the finally block.

Let’s say the SendCommandToDevice() try block runs fine, and there are no exceptions, but then the finally block throws an exception.

Here’s what happens:

2021-05-17 09:01:17.9047 level=Trace message=Start SendCommandToDevice with params: deviceId=192.168.0.2
2021-05-17 09:01:17.9359 level=Trace message=Locked device for exclusive use
2021-05-17 09:01:17.9359 level=Trace message=Attempting to connect
2021-05-17 09:01:17.9359 level=Trace message=Connected to device
2021-05-17 09:01:17.9359 level=Trace message=Attempting to send command Beep
2021-05-17 09:01:17.9359 level=Trace message=Attempted to disconnect device. It may have been disconnected already, and there's no side effects here
2021-05-17 09:01:17.9548 level=Error message=System.ArgumentException: Value does not fall within the expected range.
   at ErrorHandling.Device.Unlock() in C:\makolyte\Program.cs:line 84
   at ErrorHandling.Program.SendCommandToDevice(String deviceId, String command) in C:\makolyte\Program.cs:line 50
   at ErrorHandling.Program.Main(String[] args) in C:\makolyte\Program.cs:line 15

Code language: plaintext (plaintext)

The UnhandledException handler caught the exception, not the try/catch block. It logs the exception, and then you see the unhandled exception getting written to the console output by the system:

Unhandled exception. System.ArgumentException: Value does not fall within the expected range.
   at ErrorHandling.Device.Unlock() in C:\makolyte\Program.cs:line 85
   at ErrorHandling.Program.SendCommandToDevice(String deviceId, String command) in C:\makolyte\Program.cs:line 51
   at ErrorHandling.Program.Main(String[] args) in C:\makolyte\Program.cs:line 15Code language: plaintext (plaintext)