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.