Quick answer: Null or nil reference errors are the single most common cause of crashes in indie games. They occur when code tries to access an object that has been freed, was never initialized, or failed to load.

This guide covers common game crashes and how to prevent them in detail. Every indie developer has experienced it: you hand your build to a friend, they click play, and thirty seconds later the game is gone. No error message, no graceful exit — just a silent crash to desktop. Game crashes are the fastest way to lose a player permanently, and most of them fall into a handful of predictable categories. Here are the six most common crash types in indie games, why they happen, and how to write code that prevents them.

1. Null and Nil Reference Errors

This is the single most common crash in both Unity and Godot projects. It happens when your code tries to call a method or access a property on an object that does not exist. The object might have been freed, might not have loaded yet, or might never have been assigned in the first place.

In Unity, this surfaces as a NullReferenceException. In Godot, you will see Invalid call. Nonexistent function or Invalid get index on base Nil. Both are fatal if they happen in the wrong place — inside a physics callback, during scene loading, or in a coroutine that does not handle the null case.

Prevention in C# (Unity):

private void ApplyDamage() {
    // Bad: assumes _enemy is always valid
    // _enemy.TakeDamage(10);

    // Good: null-conditional check before access
    if (_enemy != null && _enemy.gameObject.activeInHierarchy) {
        _enemy.TakeDamage(10);
    }
}

// Even better: use TryGetComponent to avoid GetComponent null returns
if (collision.gameObject.TryGetComponent<Health>(out var health)) {
    health.TakeDamage(10);
}

Prevention in GDScript (Godot):

func apply_damage() -> void:
    # Bad: assumes enemy node still exists
    # enemy.take_damage(10)

    # Good: validate before calling
    if is_instance_valid(enemy) and enemy.is_inside_tree():
        enemy.take_damage(10)

The pattern is simple: never trust that a reference is valid. Always check before you access. This is especially critical for references that persist across frames, like cached node references, enemy targets, or UI elements that might be freed during scene transitions.

2. Stack Overflow from Infinite Recursion

Recursive functions that lack a proper base case will eat through the call stack until the engine kills the process. This is common in procedural generation code, pathfinding algorithms, and state machines where one state inadvertently triggers a transition back to itself.

// C# example: accidental infinite recursion in a property setter
public int Health {
    get => Health;  // BUG: calls itself instead of a backing field
    set => Health = value;  // Stack overflow
}

// Fix: use a backing field
private int _health;
public int Health {
    get => _health;
    set => _health = value;
}

Prevention: Always add a depth counter to recursive functions. If you are generating a dungeon with recursive room placement, pass a maxDepth parameter and decrement it on each call. When it hits zero, return. This turns a crash into a slightly under-generated level, which is always preferable to a dead process.

func generate_room(position: Vector2i, depth: int) -> void:
    if depth <= 0:
        return  # Base case prevents stack overflow
    place_room(position)
    for direction in CARDINAL_DIRECTIONS:
        if can_place_room(position + direction):
            generate_room(position + direction, depth - 1)

3. Out of Memory: Texture Leaks and Unbounded Arrays

Memory crashes are insidious because they do not happen immediately. The game runs fine for twenty minutes, then the OS kills it because it has consumed all available RAM. The two most common culprits are textures that are loaded but never freed, and dynamic arrays that grow without bound.

Texture leaks happen when you load assets for a scene but do not unload them when the scene changes. After five or six scene transitions, you have five copies of every texture in memory. Unbounded arrays happen in systems like particle trails, combat logs, or pathfinding caches where entries are appended but never removed.

Prevention: Implement explicit resource cleanup on scene exit. In Unity, call Resources.UnloadUnusedAssets() after scene transitions. In Godot, use queue_free() on nodes you no longer need and avoid holding references to freed nodes. For dynamic collections, always set a maximum size:

// C#: Ring buffer pattern for a combat log
private const int MAX_LOG_ENTRIES = 500;
private Queue<string> _combatLog = new();

public void AddLogEntry(string entry) {
    _combatLog.Enqueue(entry);
    while (_combatLog.Count > MAX_LOG_ENTRIES) {
        _combatLog.Dequeue();
    }
}

4. GPU Driver Crashes

GPU crashes are the hardest to debug because the error messages are unhelpful and the crashes are hardware-specific. A shader that works perfectly on your NVIDIA RTX card might crash on a player's Intel integrated GPU. Common triggers include unsupported shader features, exceeding VRAM limits, and driver bugs in specific vendor implementations.

Prevention: Test on at least three GPU vendors (NVIDIA, AMD, Intel). Keep shaders simple and avoid vendor-specific extensions. Provide a graphics quality setting that reduces texture resolution and disables expensive effects on lower-end hardware. If you are using compute shaders, always check for support before dispatching:

// C#: Check compute shader support before use
if (SystemInfo.supportsComputeShaders) {
    _computeShader.Dispatch(kernel, threadGroupsX, 1, 1);
} else {
    FallbackCPUCalculation();
}

Also clamp your texture quality settings based on available VRAM. A player with 2GB of VRAM should not be loading 4K textures even if they set quality to "Ultra" in the options menu.

5. Infinite Loops Freezing the Main Thread

An infinite loop does not always crash with an error. Instead, the game freezes completely. The OS may eventually kill the process after it becomes unresponsive, or the player will force-quit. Either way, the experience is identical to a crash from the player's perspective.

Infinite loops commonly appear in while-loops that wait for a condition that never becomes true, pathfinding algorithms that get stuck in cycles, and procedural generation that cannot find a valid placement.

# GDScript: Bad - can freeze if no valid position exists
var position = Vector2(0, 0)
while not is_valid_spawn(position):
    position = get_random_position()

# Good - add an iteration cap
var position = Vector2(0, 0)
var attempts = 0
while not is_valid_spawn(position):
    position = get_random_position()
    attempts += 1
    if attempts > 1000:
        push_warning("Failed to find valid spawn after 1000 attempts")
        break

The rule of thumb: every while-loop in game code should have a maximum iteration count. There are no exceptions. If your algorithm legitimately needs more than a few thousand iterations per frame, it should be spread across multiple frames using coroutines or async processing.

6. Unhandled Exceptions in File I/O and Network Code

Save file corruption, missing configuration files, failed network requests, and malformed JSON all throw exceptions. If those exceptions are not caught, the game crashes. This is especially painful for players because it often means they lose progress — the save file they were trying to load is the one that triggered the crash.

// C#: Always wrap file operations in try-catch
public SaveData LoadGame(string path) {
    try {
        var json = File.ReadAllText(path);
        return JsonSerializer.Deserialize<SaveData>(json);
    } catch (Exception ex) {
        Debug.LogError($"Failed to load save: {ex.Message}");
        return CreateDefaultSave();  // Graceful fallback
    }
}

Prevention: Treat every file read, file write, and network call as potentially failing. Return a sensible default or show a user-facing error message instead of letting the exception propagate. For save files specifically, keep a backup copy of the previous save so that a corrupted write does not destroy the player's only copy of their progress.

Building a Crash Prevention Checklist

Before every release, run through these checks:

Null safety: Search your codebase for direct object access without null checks, especially on cached references and GetComponent calls. Prioritize code paths that run every frame (process, physics) and code that executes across scene boundaries.

Recursion limits: Verify that every recursive function has a depth counter with a maximum. Check procedural generation, pathfinding, and AI decision trees.

Memory profiling: Run a 30-minute play session on your minimum-spec hardware while monitoring memory usage. If memory grows steadily without plateauing, you have a leak.

Exception handling: Audit all file I/O and network code for missing try-catch blocks. Verify that save/load has a fallback path that does not crash.

GPU testing: If possible, test on Intel, AMD, and NVIDIA hardware. At minimum, test with the lowest graphics settings your options menu allows and verify the game does not crash.

"A crash is not a bug report. It is a player deciding whether to request a refund. The difference between a 95% and 99.5% crash-free rate is the difference between Mixed and Mostly Positive reviews."

Related Issues

If you are working on reducing your crash rate before launch, our guide on reducing game crash rates covers the full process from measurement to mitigation. For understanding how to measure your progress, the crash-free sessions and stability metrics guide explains the key numbers to track and what targets to aim for.

Every crash you prevent is a player who stays. Defensive code is not paranoia — it is professionalism.