Quick answer: Hold a CancellationTokenSource as a field. Cancel it in _ExitTree. Pass _cts.Token to every Task. Long sync loops check token.IsCancellationRequested and break.

Node frees while a download is in flight. Continuation tries to update a UI label that’s now disposed. Crash. Cancellation tokens stop work cleanly when the owner exits the tree.

The Symptom

ObjectDisposedException or null reference inside an awaited continuation. Node was freed; Task didn’t notice; code ran.

The Fix

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

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

    public override async void _Ready()
    {
        try
        {
            var txt = await DownloadAsync(_cts.Token);
            GetNode<Label>("%Status").Text = txt;
        }
        catch (OperationCanceledException) { /* node freed */ }
    }

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

    private async Task<string> DownloadAsync(CancellationToken ct)
    {
        var http = new HttpClient();
        return await http.GetStringAsync("https://example.com", ct);
    }
}

_cts.Cancel in _ExitTree throws OperationCanceledException at the await point. Catch and exit silently.

Long Sync Loops

private void DoBigCpuWork(CancellationToken ct)
{
    for (int i = 0; i < 1_000_000; i++)
    {
        ct.ThrowIfCancellationRequested();
        // work
    }
}

Periodic ThrowIfCancellationRequested gives cancellation a chance to land. Throws OperationCanceledException; caller catches.

Verifying

Spawn the loader. Free it before the download completes. Console: nothing crashes; clean cancellation log. Without the token, the download finishes and crashes on the freed node.

“Token in _Ready. Cancel in _ExitTree. Pass to every Task. Cancel cleanly.”

Related Issues

For C# disposed wrapper, see disposed wrapper. For C# await background, see await background.

Token threads through. Free triggers cancel.