Skip to main content

7.1 - Understanding Errors and Exceptions

Even the most experienced programmers make mistakes. What sets professionals apart is not the absence of errors, but how they anticipate, detect, and handle them. In game development, where player experience is paramount, proper error handling can mean the difference between a minor hiccup and a game-breaking crash.

Types of Errors in C#

In C#, errors generally fall into three main categories:

1. Compile-Time Errors

Compile-time errors occur when your code violates the rules of the C# language. The compiler catches these errors before your program runs, preventing you from building your application until they're fixed.

Common compile-time errors include:

  • Syntax errors: Mistakes in the language syntax, like missing semicolons or brackets
  • Type errors: Using incompatible types, such as assigning a string to an int
  • Reference errors: Using variables or methods that don't exist
// Syntax error: missing semicolon
int playerHealth = 100
Console.WriteLine(playerHealth);

// Type error: cannot convert string to int
int score = "High Score";

// Reference error: undeclared variable
Console.WriteLine(playerLives); // playerLives doesn't exist

Compile-time errors are the easiest to fix because:

  1. The compiler tells you exactly where they are
  2. They prevent your program from running until fixed
  3. Modern IDEs highlight them as you type

2. Runtime Errors

Runtime errors occur while your program is executing. Unlike compile-time errors, these aren't detected until the problematic code is actually run. In C#, runtime errors typically manifest as exceptions.

Common runtime errors include:

  • Null reference exceptions: Trying to use an object that hasn't been initialized
  • Index out of range exceptions: Accessing an array element that doesn't exist
  • Division by zero: Attempting to divide a number by zero
  • File not found exceptions: Trying to open a file that doesn't exist
// NullReferenceException
string playerName = null;
Console.WriteLine(playerName.Length); // Crash! Can't call .Length on null

// IndexOutOfRangeException
int[] scores = new int[3];
Console.WriteLine(scores[5]); // Crash! Array only has indices 0, 1, and 2

// DivideByZeroException
int health = 100;
int damageMultiplier = 0;
int damage = health / damageMultiplier; // Crash! Division by zero

Runtime errors are more problematic because:

  1. They can occur after your game is released
  2. They cause your program to crash if not handled
  3. They might only happen under specific conditions that are hard to test

3. Logical Errors

Logical errors are the most insidious type. Your code compiles and runs without crashing, but it doesn't behave as intended. These errors stem from flaws in your algorithm or logic.

Examples of logical errors:

  • Calculating damage incorrectly in a combat system
  • Moving a character in the wrong direction
  • Applying a power-up effect to the wrong player
  • Infinite loops that never terminate
// Logical error: Healing instead of damaging
void ApplyDamage(int amount)
{
playerHealth += amount; // Should be playerHealth -= amount
}

// Logical error: Condition never met
if (playerScore = 1000) // Assignment (=) instead of comparison (==)
{
GiveExtraLife();
}

Logical errors are the hardest to find because:

  1. The compiler doesn't catch them
  2. Your program doesn't crash
  3. You might not immediately notice the incorrect behavior
  4. They require thorough testing and debugging to identify

What Are Exceptions?

Exceptions are C#'s way of handling runtime errors. When an exceptional situation occurs, the runtime "throws" an exception object that contains information about the error. If this exception isn't "caught" and handled, it will propagate up the call stack until it reaches the top level, at which point your program will crash.

The Exception Object

All exceptions in C# derive from the System.Exception class, which provides properties like:

  • Message: A human-readable description of the error
  • StackTrace: A list of method calls that led to the exception
  • InnerException: The exception that caused the current exception (if applicable)
  • Source: The name of the application or object that caused the error

The Exception Hierarchy

C# has a rich hierarchy of exception types, each representing a specific kind of error:

System.Exception
├── System.SystemException
│ ├── System.NullReferenceException
│ ├── System.IndexOutOfRangeException
│ ├── System.InvalidOperationException
│ ├── System.IO.IOException
│ ├── System.DivideByZeroException
│ └── ...
└── System.ApplicationException
└── [Custom exceptions]

Understanding this hierarchy helps you catch and handle specific types of exceptions appropriately.

Why Error Handling Matters in Game Development

In game development, error handling is particularly important for several reasons:

1. Player Experience

Games are interactive experiences that players expect to work smoothly. A crash can:

  • Interrupt immersion
  • Cause frustration
  • Lead to lost progress
  • Damage your game's reputation

2. Complex Systems

Games often involve complex, interconnected systems:

  • Physics simulations
  • AI behaviors
  • Multiplayer networking
  • Procedural generation
  • Asset loading and unloading

With so many moving parts, there are more opportunities for things to go wrong.

3. Varied Environments

Your game will run on different:

  • Hardware configurations
  • Operating systems
  • With different peripherals
  • Under different system loads

Error handling helps your game gracefully adapt to these varied conditions.

4. Live Services

Many modern games are live services that:

  • Receive regular updates
  • Connect to online servers
  • Download additional content
  • Track player statistics

Robust error handling ensures these services can recover from temporary issues.

The Cost of Ignoring Errors

Failing to handle errors properly can lead to:

  1. Game crashes: The most obvious and disruptive consequence
  2. Corrupted save data: Potentially losing players' progress
  3. Memory leaks: Gradually degrading performance
  4. Security vulnerabilities: Especially in networked games
  5. Difficult debugging: Problems that are hard to reproduce and fix
  6. Poor reviews: Affecting your game's commercial success

Error Handling Philosophy

When approaching error handling in game development, consider these principles:

1. Fail Fast, Fail Safely

Detect errors as early as possible, but handle them in a way that minimizes disruption to the player.

2. Graceful Degradation

When something goes wrong, try to continue with reduced functionality rather than crashing completely.

3. Informative Feedback

Let players know what went wrong and what they can do about it, without exposing technical details.

4. Log Everything

Maintain detailed logs of errors for debugging, even in release builds.

5. Anticipate the Unexpected

Think about what could go wrong and plan for it, especially in critical game systems.

Example: Error Scenarios in Games

Let's look at some common error scenarios in games and how they might be handled:

Asset Loading Errors

public Texture2D LoadTexture(string texturePath)
{
try
{
// Attempt to load the texture
return Resources.Load<Texture2D>(texturePath);
}
catch (Exception e)
{
// Log the error
Debug.LogError($"Failed to load texture: {texturePath}. Error: {e.Message}");

// Return a fallback texture instead of crashing
return defaultTexture;
}
}

Network Connection Issues

public void ConnectToServer()
{
try
{
networkClient.Connect(serverAddress, serverPort);
}
catch (SocketException e)
{
// Show a user-friendly message
uiManager.ShowErrorMessage("Could not connect to the game server. Please check your internet connection and try again.");

// Log detailed information for debugging
Debug.LogError($"Network connection failed: {e.Message}");
}
}

Save Game Corruption

public PlayerData LoadPlayerData()
{
try
{
string json = File.ReadAllText(saveFilePath);
return JsonUtility.FromJson<PlayerData>(json);
}
catch (FileNotFoundException)
{
Debug.Log("No save file found. Creating new player data.");
return new PlayerData();
}
catch (JsonException)
{
Debug.LogWarning("Save file appears to be corrupted. Creating backup and starting fresh.");

// Create a backup of the corrupted file
if (File.Exists(saveFilePath))
{
File.Copy(saveFilePath, saveFilePath + ".bak", true);
}

return new PlayerData();
}
}

Unity-Specific Error Handling

Unity provides several tools for error handling:

Debug Class

The Debug class offers methods for logging information, warnings, and errors:

Debug.Log("Player spawned successfully");
Debug.LogWarning("Health is low, consider healing");
Debug.LogError("Failed to load level data");

These messages appear in the Unity Console window with appropriate coloring and can be filtered by type.

Exception Handling in MonoBehaviour

Unity's component lifecycle methods (like Update, Start, etc.) have built-in exception handling. If an exception occurs in these methods, Unity will:

  1. Log the exception to the console
  2. Disable the component that threw the exception
  3. Continue running the game

While this prevents immediate crashes, it can lead to subtle bugs if components stop functioning without you realizing why.

Unity Relevance

Unity 6.x includes improved error reporting and debugging tools, including better stack traces and more informative error messages. The Unity Editor will also highlight scripts with errors more clearly in the Inspector.

Conclusion

Understanding the different types of errors and how exceptions work is the first step toward building robust, player-friendly games. In the next sections, we'll explore the practical tools C# provides for handling exceptions, starting with the fundamental try-catch blocks.

Remember: Good error handling isn't just about preventing crashes—it's about creating a seamless experience for your players, even when things don't go exactly as planned.