Quick answer: The C# reference outlives the Godot object after QueueFree. Always call GodotObject.IsInstanceValid(node) before access. Pair with null check; both should pass before using the node.

Here is how to fix Godot 4 C# code throwing GodotObject already disposed when you access a Node after it was freed. C# garbage collection is independent from Godot’s object lifetime; once Godot frees a Node, your C# reference becomes a stale pointer until GC eventually clears it.

The Symptom

Calling Position.Set or any other property on a stored Node reference throws System.ObjectDisposedException: Cannot access a disposed object. The reference is non-null in C# but the underlying Godot resource is gone.

What Causes This

QueueFree completed. Godot frees the C++ object; C# reference dangles.

Cross-scene references. Nodes from a previous scene held in autoload state become invalid after the scene unloads.

Async callbacks. A timer fires after the target was freed.

The Fix

Step 1: Always check IsInstanceValid before access.

using Godot;

public partial class PlayerController : Node
{
    private Node target;

    public override void _Process(double delta)
    {
        if (target == null) return;
        if (!GodotObject.IsInstanceValid(target))
        {
            target = null;   // clean up our reference
            return;
        }

        // safe to use target
        target.Set("position", new Vector2(100, 100));
    }
}

Step 2: Subscribe to TreeExited / TreeExiting.

target.TreeExiting += () => { target = null; };

Receives notice when the target is about to leave the tree; clear your reference proactively.

Step 3: Use weak references for long-lived holders.

using System;

private WeakReference<Node> targetRef;

public void SetTarget(Node t) { targetRef = new WeakReference<Node>(t); }

public Node GetTarget()
{
    if (targetRef != null && targetRef.TryGetTarget(out var n) && GodotObject.IsInstanceValid(n))
        return n;
    return null;
}

WeakReference does not prevent Godot from freeing the target.

Step 4: Defer access in callbacks. If a callback might fire after the target was freed, defer:

CallDeferred(nameof(DoTheThing));

Deferred calls run on the next idle frame; intervening QueueFrees apply first.

Step 5: Avoid long-lived references in autoloads. Autoloads outlive scenes. Storing per-scene Node references in an autoload is a leak waiting to happen.

“C# reference != Godot object. IsInstanceValid every access. Null after TreeExited. No more disposed exceptions.”

Related Issues

For C# signal disconnect, see C# Signal Disconnect. For C# export build issues, see C# Export Build.

IsInstanceValid. WeakReference for holders. CallDeferred for safety. Disposed errors stop.