Skip to main content

7.3 - finally Block

In the previous section, we explored how to use try-catch blocks to handle exceptions. Now, we'll learn about the finally block, which provides a way to execute code regardless of whether an exception occurred.

Understanding the finally Block

The finally block contains code that will run:

  • After the try block completes successfully, or
  • After an exception is caught and handled in a catch block, or
  • Before an exception propagates up the call stack (if it wasn't caught)

Here's the basic syntax:

try
{
// Code that might throw an exception
}
catch (Exception e)
{
// Code to handle the exception
}
finally
{
// Code that always runs, regardless of whether an exception occurred
}

Why finally Is Important

The finally block serves several critical purposes:

1. Resource Cleanup

It ensures resources are properly released, even if exceptions occur:

  • Closing files
  • Releasing network connections
  • Disposing of unmanaged resources
  • Returning objects to pools
  • Releasing locks

2. State Restoration

It allows you to restore program state:

  • Resetting flags
  • Restoring UI elements
  • Completing transactions
  • Returning to previous game states

3. Code Simplification

It reduces code duplication by centralizing cleanup logic that would otherwise need to be repeated in both the normal execution path and exception handlers.

Basic Example

Let's start with a simple example to illustrate how finally works:

public void ProcessPlayerInput()
{
bool inputLocked = false;

try
{
// Lock input while processing
inputLocked = true;
Debug.Log("Input locked");

// Process input (might throw an exception)
ProcessComplexInput();

Debug.Log("Input processing completed successfully");
}
catch (Exception e)
{
Debug.LogError($"Error processing input: {e.Message}");
}
finally
{
// Always unlock input, even if an exception occurred
inputLocked = false;
Debug.Log("Input unlocked");
}
}

In this example:

  • We lock input before processing
  • If processing succeeds, we reach the finally block and unlock input
  • If an exception occurs, we handle it in the catch block, then reach the finally block and unlock input
  • Even if we didn't catch the exception, the finally block would still execute before the exception propagates up

Game Development Examples

Let's look at some practical examples of using finally in game development scenarios:

1. File Operations

public bool SaveGameData(GameData data, string filePath)
{
FileStream fileStream = null;

try
{
// Create a file stream
fileStream = new FileStream(filePath, FileMode.Create);

// Serialize the game data to the file
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(fileStream, data);

return true;
}
catch (IOException e)
{
Debug.LogError($"Error saving game data: {e.Message}");
return false;
}
finally
{
// Always close the file stream, even if an exception occurred
if (fileStream != null)
{
fileStream.Close();
Debug.Log("File stream closed");
}
}
}

2. Loading Screen Management

public async Task LoadLevelAsync(string levelName)
{
// Show loading screen
uiManager.ShowLoadingScreen();

try
{
// Attempt to load the level
await SceneManager.LoadSceneAsync(levelName);

// Initialize level components
InitializeLevelSystems();
}
catch (Exception e)
{
Debug.LogError($"Failed to load level '{levelName}': {e.Message}");
uiManager.ShowErrorMessage("Failed to load level. Returning to main menu.");
await SceneManager.LoadSceneAsync("MainMenu");
}
finally
{
// Always hide the loading screen, even if an exception occurred
uiManager.HideLoadingScreen();
}
}

3. Animation State Management

public void PlayAttackAnimation()
{
// Store the previous animation state
string previousState = animator.GetCurrentAnimatorStateInfo(0).shortNameHash.ToString();

try
{
// Set parameters for attack animation
animator.SetBool("IsAttacking", true);

// Play the attack sound
audioSource.PlayOneShot(attackSound);

// Apply damage to targets
ApplyDamageToTargetsInRange();
}
catch (Exception e)
{
Debug.LogError($"Error during attack: {e.Message}");
}
finally
{
// Always reset the attack state, even if an exception occurred
animator.SetBool("IsAttacking", false);
Debug.Log("Attack animation state reset");
}
}

4. Game State Transitions

public void EnterBossPhase()
{
// Store the current game state
GameState previousState = gameStateManager.CurrentState;

try
{
// Change to boss phase state
gameStateManager.SetState(GameState.BossFight);

// Lock doors
LockAllDoors();

// Start boss AI
bossController.Activate();

// Play boss music
audioManager.PlayBossMusic();
}
catch (Exception e)
{
Debug.LogError($"Failed to enter boss phase: {e.Message}");

// Try to recover by returning to the previous state
gameStateManager.SetState(previousState);
}
finally
{
// Always update UI elements regardless of success or failure
uiManager.UpdateBossHealthBar(bossController.IsActive);
uiManager.ShowBossBanner(bossController.BossName);
}
}

5. Object Pooling

public GameObject GetPooledObject(string objectType)
{
GameObject obj = null;
bool objectReserved = false;

try
{
// Try to get an object from the pool
obj = objectPool.GetObject(objectType);

// Mark the object as reserved
objectReserved = true;
Debug.Log($"Reserved object of type {objectType}");

// Configure the object for use
ResetObjectProperties(obj);
ConfigureObjectForCurrentLevel(obj);

return obj;
}
catch (Exception e)
{
Debug.LogError($"Error getting pooled object: {e.Message}");
return null;
}
finally
{
// If we reserved an object but something failed during configuration,
// return it to the pool
if (objectReserved && obj == null)
{
objectPool.ReturnObject(obj);
Debug.Log("Returned partially configured object to pool");
}
}
}

finally Without catch

You can use a finally block without a catch block when you want to ensure cleanup happens but don't want to handle the exception at the current level:

public void PlayCutscene()
{
// Disable player controls
playerController.enabled = false;

try
{
// Play the cutscene
cutsceneManager.PlayCutscene("intro");
}
finally
{
// Always re-enable player controls, even if the cutscene fails
playerController.enabled = true;
}

// If an exception occurs in PlayCutscene, it will propagate up
// AFTER the finally block executes
}

This pattern is useful when:

  • You want to ensure cleanup happens
  • You don't have a specific recovery strategy at this level
  • You want to let a higher-level handler deal with the exception

Control Flow with finally

It's important to understand how finally affects control flow:

public int CalculateDamage()
{
try
{
Debug.Log("Calculating damage...");
return 50; // This return is "pending" until finally completes
}
finally
{
Debug.Log("Damage calculation complete");
// The finally block executes before the return happens
}

// This code is unreachable
}

When a return, break, or continue statement appears in a try or catch block, the finally block still executes before the control is transferred.

Important Behavior Notes

  1. Return values are "remembered": If a try or catch block has a return statement, the return value is stored, the finally block executes, and then the method returns the stored value.

  2. finally can't change the return value: If you assign a new value to the same variable in the finally block, it won't affect the already-stored return value.

  3. Exceptions in finally: If the finally block throws an exception, it will override any exception from the try or catch blocks.

public void DangerousOperation()
{
try
{
// This throws an exception
throw new InvalidOperationException("Operation failed");
}
finally
{
// This throws another exception
throw new Exception("Cleanup failed");
}

// The caller will only see "Cleanup failed" exception
// The "Operation failed" exception is lost
}

Common Pitfalls with finally

1. Throwing Exceptions in finally

As shown above, throwing exceptions in a finally block can mask previous exceptions. Avoid this pattern:

// Bad practice
try
{
// Operation that might fail
LoadGameData();
}
finally
{
try
{
// Cleanup that might also fail
CloseDataConnection();
}
catch (Exception e)
{
// Log but don't rethrow
Debug.LogError($"Cleanup error: {e.Message}");
}
}

2. Returning from finally

Avoid returning from a finally block, as it will override any return value from the try or catch blocks:

// Bad practice
public bool TryOperation()
{
try
{
// Operation succeeds
return true;
}
catch
{
// Operation fails
return false;
}
finally
{
// This overrides both return values above!
return false;
}
}

3. Complex Logic in finally

Keep finally blocks focused on cleanup. Avoid complex logic that might itself fail:

// Bad practice
try
{
ProcessGameData();
}
finally
{
// Too much complex logic in finally
SaveGameState();
UpdatePlayerUI();
RecalculatePathfinding();
OptimizeMemory();
}

finally vs. using Statement

For resource cleanup, C# provides the using statement as an alternative to try-finally (which we'll cover in the next section). Compare:

// With try-finally
FileStream file = null;
try
{
file = File.OpenRead("savegame.dat");
// Use the file
}
finally
{
if (file != null)
{
file.Dispose();
}
}

// With using statement (cleaner)
using (FileStream file = File.OpenRead("savegame.dat"))
{
// Use the file
// Automatically disposed when the block exits
}

The using statement is more concise and less error-prone for disposable resources.

Unity-Specific Considerations

In Unity, there are some specific scenarios where finally blocks are particularly useful:

1. Coroutine Error Handling

Coroutines don't have built-in exception handling, so you need to be careful:

public IEnumerator LoadLevelCoroutine(string levelName)
{
bool loadingScreenActive = false;

try
{
// Show loading screen
uiManager.ShowLoadingScreen();
loadingScreenActive = true;

// Wait for resources to load
yield return StartCoroutine(PreloadLevelResources(levelName));

// Load the scene
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync(levelName);
while (!asyncLoad.isDone)
{
uiManager.UpdateLoadingProgress(asyncLoad.progress);
yield return null;
}
}
catch (Exception e)
{
Debug.LogError($"Error loading level: {e.Message}");
// Handle the error
}
finally
{
// Always hide the loading screen
if (loadingScreenActive)
{
uiManager.HideLoadingScreen();
}
}
}

// Usage with try-catch to handle exceptions in the coroutine
public void LoadLevel(string levelName)
{
try
{
StartCoroutine(LoadLevelCoroutine(levelName));
}
catch (Exception e)
{
Debug.LogError($"Failed to start level loading: {e.Message}");
}
}

2. Editor Script Cleanup

When writing Unity editor scripts, finally blocks can ensure the editor state is restored:

public void ProcessSelectedObjects()
{
// Store the current selection
GameObject[] previousSelection = Selection.gameObjects;

try
{
// Modify objects in the scene
foreach (GameObject obj in Selection.gameObjects)
{
ProcessGameObject(obj);
}
}
catch (Exception e)
{
Debug.LogError($"Error processing objects: {e.Message}");
EditorUtility.DisplayDialog("Error", "Failed to process selected objects.", "OK");
}
finally
{
// Restore the previous selection
Selection.objects = previousSelection;

// Ensure the scene is marked as dirty
EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
}
}
Unity Relevance

Unity's serialization and deserialization processes can sometimes throw exceptions, especially when dealing with custom serializable classes. Using finally blocks can help ensure your game recovers gracefully from serialization errors, particularly when loading saved games or prefab instances.

Conclusion

The finally block is a powerful tool for ensuring that cleanup code always runs, regardless of whether exceptions occur. By using it effectively, you can:

  • Ensure resources are properly released
  • Maintain consistent game state
  • Simplify your error handling code
  • Make your game more robust against failures

In the next section, we'll explore the using statement, which provides a more concise way to handle disposable resources with automatic cleanup.