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 thefinally
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
-
Return values are "remembered": If a
try
orcatch
block has areturn
statement, the return value is stored, thefinally
block executes, and then the method returns the stored value. -
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. -
Exceptions in finally: If the
finally
block throws an exception, it will override any exception from thetry
orcatch
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'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.