GDKB

Exceptions

Intermediate25 Minutes readUpdated July 21, 20263 related concepts

An exception is a signal that something went wrong during execution — a missing file, an invalid argument, dividing by zero — that interrupts normal control flow and propagates upward until something explicitly catches and handles it, or the program terminates. Exceptions exist so error handling doesn't have to be manually threaded through every return value in a call chain.

On this page

Exceptions

Summary

An exception is a signal that something went wrong during execution — a missing file, an invalid argument, dividing by zero — that interrupts normal control flow and propagates upward until something explicitly catches and handles it, or the program terminates. Exceptions exist so error handling doesn't have to be manually threaded through every return value in a call chain.

Mental Model

Think of a normal function call chain as a relay race: each function does its work and hands the baton to the next. An exception is like a runner suddenly shouting "stop the race!" and refusing to hand off the baton normally — instead, they hand up a note explaining what went wrong, and that note skips every runner ahead of it in the chain until someone specifically says "I'll read that note" (a catch block). If nobody ever says that, the note reaches the race organizer (the runtime), who stops the whole race (crashes the program) and prints the note for everyone to see.

This is fundamentally different from a function returning false or -1 to signal failure: a return value only reaches its immediate caller and is trivially ignorable, while an unhandled exception forcibly interrupts every level of the call stack until it's dealt with.

Core Explanation

  • Throwing. throw new ArgumentException("amount must be positive"); creates an exception object and immediately halts normal execution at that point, unwinding the call stack.
  • Propagation (unwinding). If the function that threw doesn't handle it, the exception moves up to its caller, then that caller's caller, and so on — each frame's remaining code in that call is skipped — until a matching catch is found or the stack runs out.
  • Catching. A try { ... } catch (ArgumentException ex) { ... } block intercepts a matching exception type, letting the program recover (retry, log, show an error, substitute a default) instead of crashing. Multiple catch clauses can handle different exception types differently; a broader catch (Exception ex) catches anything not already matched.
  • finally. Code in a finally block runs whether or not an exception was thrown — the standard place to release a resource (close a file, unsubscribe an event) that must clean up either way.
  • Cost. Throwing and catching an exception is meaningfully slower than a normal return in most languages, because unwinding the stack and capturing debug information (a stack trace) is real work — this is why exceptions are for exceptional situations, not routine control flow (see Common Mistakes).

Syntax / Examples

C# (Unity)

csharp
public class SaveLoader : MonoBehaviour
{
    public PlayerData Load(string path)
    {
        try
        {
            string json = System.IO.File.ReadAllText(path);
            return JsonUtility.FromJson<PlayerData>(json);
        }
        catch (System.IO.FileNotFoundException)
        {
            Debug.LogWarning($"No save found at {path}, using defaults.");
            return new PlayerData();
        }
        catch (System.Exception ex)
        {
            Debug.LogError($"Save file at {path} is corrupted: {ex.Message}");
            return new PlayerData();
        }
        finally
        {
            Debug.Log("Load attempt finished.");
        }
    }
}

C++ (Unreal)

Unreal Engine builds with C++ exceptions disabled by default for performance and platform-compatibility reasons; error handling in Unreal C++ conventionally uses return codes, bool out-parameters, or TOptional instead:

cpp
bool USaveLoader::TryLoad(const FString& Path, FPlayerData& OutData)
{
    FString Json;
    if (!FFileHelper::LoadFileToString(Json, *Path))
    {
        UE_LOG(LogTemp, Warning, TEXT("No save found at %s, using defaults."), *Path);
        OutData = FPlayerData();
        return false;
    }

    return FJsonObjectConverter::JsonObjectStringToUStruct(Json, &OutData);
}

Unreal Blueprint

Blueprint has no try/catch construct; the equivalent pattern is a function with a Success boolean output pin (or a Branch node checking a validity/error output), matching the C++ return-code convention above rather than the C# try/catch convention.

Practical Example

Parsing a player-entered number (a chat command argument, a settings menu text field) is a common source of exceptions that must be handled instead of allowed to crash the game.

csharp
public bool TryParsePlayerCount(string input, out int count)
{
    try
    {
        count = int.Parse(input);
        if (count < 1 || count > 8)
        {
            Debug.LogWarning($"Player count {count} out of range.");
            count = 0;
            return false;
        }
        return true;
    }
    catch (System.FormatException)
    {
        Debug.LogWarning($"'{input}' is not a valid number.");
        count = 0;
        return false;
    }
}

Note that the out-of-range check does not throw — it's an expected, routine outcome of user input, handled with a normal if/return, exactly as Common Mistakes recommends below. Only the genuinely unexpected case — text that isn't a number at all — goes through the catch.

Common Mistakes

  • Using exceptions for expected, routine control flow. Throwing to signal "inventory is full" or "player pressed Cancel" — outcomes that happen constantly and are entirely expected — is slow and makes normal logic paths hard to follow. Return a bool/enum/Result-style value for anything that isn't genuinely exceptional.
  • Catching Exception broadly and silently swallowing it. catch (Exception) { } with an empty body hides real bugs — the failure disappears with no log, no stack trace, nothing to debug later. At minimum, log what was caught.
  • Catching an exception far too broadly around a large block of code, making it unclear which specific line actually failed. Keep try blocks as small as practical around the operation that can actually throw.
  • Not cleaning up resources when an exception occurs mid-operation — a file left open, an event handler left subscribed — because the code after the throwing line was skipped. Use finally (or a using block, which is finally under the hood) for anything that must be released regardless of success or failure.

Best Practices

  • Reserve exceptions for genuinely unexpected failures (corrupted data, missing files, programmer errors like a null argument) — use return values or TryX(out result) patterns for expected, routine outcomes.
  • Catch the most specific exception type that's meaningful to handle differently, and let anything else propagate rather than guessing at a generic recovery.
  • Always log (at minimum) any exception that's caught and not rethrown, so a swallowed failure doesn't disappear without a trace.
  • Use finally (or using) for cleanup that must happen whether or not an exception occurred, rather than duplicating cleanup code in both the success and catch paths.

Exercises

  1. Write TryParsePlayerCount (as above) and then a second version that throws a custom exception instead of returning bool. Argue which version is the better fit for validating a settings-menu text field, using the "expected vs. exceptional" distinction from this document.
  2. Reproduce an unhandled NullReferenceException on purpose in a small script, observe what the console reports, then wrap the call in a try/catch and compare the program's behavior with and without it.
  3. Take the SaveLoader.Load example and add a finally block that logs how long the load attempt took, regardless of success or failure. Explain why finally is the right place for this rather than the end of the try block.
  4. Explain, for an Unreal C++ project with exceptions disabled, how the TryLoad-with-bool-return pattern achieves the same practical outcome as a C# try/catch, and what it cannot express that exceptions can.
  • Debugging — reading a stack trace produced by an unhandled or logged exception is one of the most common starting points for a debugging session.
  • Classes — custom exception types are ordinary classes that inherit from a base exception class, carrying whatever extra data the failure needs to describe.
  • Strings — parsing and validating string input from players or files is one of the most common sources of exceptions in a game codebase.

References

Related Concepts