Quick answer: await ToSignal(emitter, name) resumes only when that exact signal fires on a live emitter. Verify the name string, that the emitter isn’t freed, and add a timeout race for safety.

A C# coroutine does await ToSignal(enemy, "Died") and never continues — the enemy was freed before dying, or the signal name is wrong.

Exact Signal Name

// brittle: typo = hangs forever
await ToSignal(enemy, "Died");

// safe: use the generated SignalName constant
await ToSignal(enemy, Enemy.SignalName.Died);

The generated SignalName constants are compile-checked — a rename breaks the build instead of silently hanging.

Emitter Must Stay Alive

If the emitter is QueueFree’d before emitting, the await never completes — the awaiting state machine just sits there. Don’t await a signal on something you might destroy first.

Race With a Timeout

var signalTask = ToSignal(enemy, Enemy.SignalName.Died);
var timeout = ToSignal(GetTree().CreateTimer(5.0), SceneTreeTimer.SignalName.Timeout);
await Task.WhenAny(signalTask.AsTask(), timeout.AsTask());

Whichever fires first resumes you. The await can never hang indefinitely.

Cancellation on Tree Exit

If the awaiting node itself leaves the tree, guard the continuation with if (!IsInsideTree()) return; after the await — otherwise you touch a node that’s gone.

Verifying

The coroutine resumes when the enemy dies. If the enemy is removed another way, the timeout fires and the coroutine continues gracefully. No permanent hangs.

“ToSignal resumes only on the exact signal from a live emitter. Use SignalName constants and race a timeout.”

Make ‘await with timeout’ a small helper extension — you’ll want it everywhere once you start awaiting signals.