Quick answer: The await never resumes because the signal is never emitted. The most common causes are the emitting object being freed before it can emit, a typo in the signal name, or a conditional code path that never reaches the emit() call.
Here is how to fix Godot GDScript await signal never completing. You write await enemy.died and your function just stops. No error, no crash, no timeout. The code after the await line never executes, and the game continues running as if that function no longer exists. You add print statements before and after — the one before prints, the one after never does. The await keyword is working correctly; it is patiently waiting for a signal that will never arrive. The question is why the signal never emits.
The Symptom
A function containing await some_object.some_signal executes up to the await line and then permanently suspends. The rest of the function — including cleanup code, state transitions, and return values — never runs. There is no error message in the Output panel. The game does not freeze; other scripts and nodes continue operating normally. Only the specific coroutine that hit the await is stuck.
This is different from a deadlock or infinite loop. The function is not spinning — it is genuinely suspended, consuming no CPU time, waiting for a signal emission that will never happen. If you are tracking active coroutines, you will see the count increase over time as each call to the function creates a new suspended coroutine that is never cleaned up.
What Causes This
The emitting object is freed before it emits. This is the most common cause by far. You await enemy.died, but somewhere else in your code, the enemy is removed with queue_free() before it gets a chance to emit died. When a node is freed, all its signals are disconnected and can never emit again. The coroutine waiting on that signal is abandoned. Godot does not resume it with a default value or raise an error — it simply becomes unreachable.
This frequently happens with enemies that are removed by a different system than the one that kills them. For example, an enemy walks off-screen and is culled by a visibility notifier, but the combat system is still awaiting its death signal. Or a scene transition frees all children of the current level while a cutscene script is awaiting an NPC’s animation signal.
Signal name typo. GDScript does not validate signal names at the await call site the way it validates method names. If you declare signal attack_finished but write await enemy.attack_complete, Godot will not warn you. The await listens for a signal called attack_complete that does not exist on the object, so it waits forever. This is especially easy to miss when signals are defined in a parent class and used in a subclass, or when signal names change during refactoring.
Conditional emit paths. The signal is defined and the object is alive, but the code that calls emit() is inside a conditional branch that never executes. For example, a health component only emits died when health <= 0, but due to a bug in the damage calculation, health never actually reaches zero — it goes to 1 and stays there. Or an animation player emits a signal at the end of an animation, but the animation is interrupted before reaching that frame.
The Fix
Step 1: Guard against freed objects. Before awaiting a signal from an object that might be freed, check that it is valid. Better yet, connect to the tree_exiting signal as a safety net so you know if the object is removed before emitting.
signal died
func wait_for_enemy_death(enemy: Node) -> void:
if !is_instance_valid(enemy):
push_warning("Enemy already freed before await")
return
# Race between the signal and the object being freed
var result = await _await_or_freed(enemy, "died")
if result == "freed":
push_warning("Enemy freed before emitting 'died'")
return
print("Enemy died normally")
func _await_or_freed(obj: Node, sig_name: StringName) -> String:
var sig = Signal(obj, sig_name)
var freed_sig = obj.tree_exiting
# Whichever fires first resumes the coroutine
var completed := false
var freed := false
sig.connect(func(): completed = true, CONNECT_ONE_SHOT)
freed_sig.connect(func(): freed = true, CONNECT_ONE_SHOT)
while !completed and !freed:
await get_tree().process_frame
return "freed" if freed else "completed"
The tree_exiting signal fires right before a node is removed from the tree, which happens before queue_free() actually destroys it. By racing the target signal against tree_exiting, you guarantee the coroutine always resumes, one way or another.
Step 2: Validate signal names at runtime. Add a debug check that verifies the signal exists on the target object before awaiting it. This turns a silent hang into a loud error during development.
func safe_await(obj: Object, sig_name: StringName) -> Variant:
if !obj.has_signal(sig_name):
push_error("Object %s has no signal '%s'" % [obj, sig_name])
return null
if !is_instance_valid(obj):
push_error("Object is freed, cannot await signal")
return null
return await Signal(obj, sig_name)
The has_signal() method checks whether the signal is declared on the object. If someone renames died to death but forgets to update all the await sites, this check catches it immediately. Use this wrapper during development and strip it in release builds if the overhead concerns you.
Handling Conditional Emit Paths
If the signal is only emitted under certain conditions, you need to ensure there is always an exit path. The cleanest approach is to emit the signal in every branch, possibly with different arguments to indicate what happened.
For example, instead of only emitting died when health reaches zero, emit a combat_resolved signal in all cases — death, retreat, despawn, or timeout. The awaiting code checks the result and handles each case. This eliminates dead paths where no signal is ever emitted.
For animations, use the animation_finished signal from AnimationPlayer rather than relying on a custom signal embedded in an animation track. The animation_finished signal fires even if the animation is interrupted (in Godot 4.x, you can check the finished animation name). If you must use track-based signals, also connect to animation_changed as a fallback so you know when the animation is interrupted before reaching your signal frame.
Timeout Pattern
For situations where you cannot guarantee the signal will ever emit, add a timeout. This prevents coroutines from hanging indefinitely in production.
Create a SceneTreeTimer with get_tree().create_timer(timeout_seconds) and race it against the target signal. If the timer fires first, log a warning and continue execution. This is especially important for network-related signals, signals from external plugins, and signals tied to player actions that may never happen.
A reasonable timeout for gameplay signals is 30 to 60 seconds. For UI signals (button presses, menu selections), a timeout may not make sense — instead, connect to the tree_exiting signal of the UI node to handle the case where the player closes the menu without making a selection.
“Every
awaitin your code is a promise that a signal will fire. If you cannot guarantee that promise, you need a fallback. Silent hangs are worse than crashes because nobody notices them until the game ships.”
Related Issues
If your signals are emitting but connected callbacks are not firing, see Signal Not Connecting at Runtime for connection ordering and one-shot flag issues. If await get_tree().create_timer() itself is not completing, check SceneTree Timer Not Firing for paused tree and process mode problems.
Use has_signal() before every await during development — it catches typos that would otherwise hang silently.