Quick answer: After every await, check IsInsideTree() and GodotObject.IsInstanceValid(this). Use a CancellationToken cancelled in _ExitTree. Catch OperationCanceledException to exit cleanly.

Async download completes; you set a property on the owning node. Crash — the node was removed from the tree or queue_freed.

The Symptom

ObjectDisposedException or null property access after an await. Hard to reproduce because timing-dependent.

The Fix

using System.Threading;
using System.Threading.Tasks;

public partial class Loader : Node
{
    private readonly CancellationTokenSource _cts = new();

    public override async void _Ready()
    {
        try
        {
            var data = await DownloadAsync(_cts.Token);

            if (!IsInsideTree() || !IsInstanceValid(this)) return;

            GetNode<Label>("%Status").Text = data;
        }
        catch (OperationCanceledException) { }
    }

    public override void _ExitTree()
    {
        _cts.Cancel();
        _cts.Dispose();
    }
}

Cancel on exit; check liveness on resume; catch the cancellation.

Verifying

Spawn the loader. queue_free it before the download completes. Should log nothing or a benign “cancelled” — never crash.

“Token. Validate. Catch cancel. Awaits stay safe.”

Related Issues

For C# CancellationToken, see CancellationToken. For C# disposed wrapper, see disposed wrapper.

Validate after await. Crashes vanish.