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.