Quick answer: Always call signal.is_connected(callable) before disconnect(), and make sure you pass the exact same Callable reference that was used in connect()—lambda functions create a new Callable object every time they are written, so store them in a variable.
You call my_signal.disconnect(handler) to clean up a connection and Godot fires back with ERROR: Invalid connection—or worse, your game crashes with a cryptic stack trace. The connection seemed perfectly normal when you set it up. What went wrong?
The Symptom
The error message from Godot looks like one of these:
ERROR: Signal 'timeout' does not have any connections.ERROR: Invalid connection.- A silent failure where
disconnect()appears to succeed but the handler still fires on the next emission. - An editor warning: “Disconnecting a signal from a freed object.”
All of these point to a mismatch between the callable stored in the connection and the callable you are passing to disconnect().
What Causes This
The callable passed to disconnect() doesn’t match the one used in connect(). Godot identifies signal connections by their Callable object. Two callables are equal only if they refer to the same method on the same object. This sounds obvious, but it’s easy to get wrong—especially with lambdas.
Using a lambda inline in both connect() and disconnect(). In GDScript, the expression func(): do_something() creates a new Callable object each time it is evaluated. If you write the same lambda expression in two places, you have two different objects that are not equal, even if their code is identical. Godot will not find the connection and will throw “Invalid connection.”
Calling disconnect() on a signal connected with CONNECT_ONE_SHOT. The CONNECT_ONE_SHOT flag automatically removes the connection after the signal fires once. If the signal has already fired by the time you call disconnect(), the connection is already gone and your call will fail.
Calling disconnect() on an already-freed object. If the source or target node was freed (for example, it left the scene tree and was queue-freed), Godot removes its connections automatically. Trying to manually disconnect them afterward fails because they no longer exist.
The Fix: Guard with is_connected()
The simplest and most defensive fix is to always check before disconnecting:
# Safe disconnect pattern
if my_signal.is_connected(handler):
my_signal.disconnect(handler)
This pattern works for named methods, bound callables, and any situation where you aren’t 100% sure the connection is still active.
The Object class also exposes a string-based form that is equivalent:
# Also valid — string-based lookup
if is_connected("timeout", _on_timer_timeout):
disconnect("timeout", _on_timer_timeout)
Prefer the signal-variable form (signal.is_connected(callable)) in Godot 4 because it avoids typos in string names and benefits from the editor’s static analysis.
Fixing Lambda Disconnect Issues
If you connected a lambda and need to disconnect it later, store the lambda in a member variable:
extends Node
var _on_damage_taken: Callable # Store the lambda reference
func _ready() -> void:
_on_damage_taken = func(amount: int):
health -= amount
update_health_bar()
player.damage_taken.connect(_on_damage_taken)
func cleanup() -> void:
if player.damage_taken.is_connected(_on_damage_taken):
player.damage_taken.disconnect(_on_damage_taken)
Without storing the lambda in _on_damage_taken, the two inline func expressions would be separate Callable objects and is_connected() would return false.
If you need a one-time lambda and don’t want to manage disconnecting at all, use CONNECT_ONE_SHOT:
# Fires once, then auto-disconnects — no manual cleanup needed
$AnimationPlayer.animation_finished.connect(
func(anim_name: String): on_intro_done(),
CONNECT_ONE_SHOT
)
Do not call disconnect() on a CONNECT_ONE_SHOT connection after the signal has fired—the connection is already gone. If you might call disconnect() before or after firing, always guard with is_connected().
Bound Callables and disconnect()
When you use Callable.bind() to attach arguments to a connection, the bound values become part of the Callable identity. You must pass the exact same bound callable to disconnect():
extends Node
var _bound_handler: Callable
func _ready() -> void:
_bound_handler = _on_item_collected.bind("sword")
inventory.item_collected.connect(_bound_handler)
func _exit_tree() -> void:
if inventory.item_collected.is_connected(_bound_handler):
inventory.item_collected.disconnect(_bound_handler)
func _on_item_collected(item_name: String) -> void:
print("Picked up: ", item_name)
Calling disconnect(_on_item_collected.bind("sword")) without storing the callable first will fail, because .bind() creates a new Callable object each time it is called.
Using _exit_tree() for Reliable Cleanup
A practical pattern for nodes that connect to signals on other nodes is to always disconnect in _exit_tree(). By the time _exit_tree() fires, both the source and target nodes are still valid (not yet freed), making it the safest place to call disconnect():
extends Node
func _ready() -> void:
GameEvents.enemy_died.connect(_on_enemy_died)
GameEvents.level_completed.connect(_on_level_completed)
func _exit_tree() -> void:
if GameEvents.enemy_died.is_connected(_on_enemy_died):
GameEvents.enemy_died.disconnect(_on_enemy_died)
if GameEvents.level_completed.is_connected(_on_level_completed):
GameEvents.level_completed.disconnect(_on_level_completed)
func _on_enemy_died() -> void:
score += 100
func _on_level_completed() -> void:
show_level_complete_screen()
Note that if you are connecting to signals on nodes that are guaranteed to be freed before your node, Godot will clean up those connections automatically. Manual disconnection in _exit_tree() is primarily important for autoload singletons and other long-lived signal sources that outlive individual scene nodes.
Checking Connections Programmatically
To inspect all current connections on a signal at runtime—useful for debugging duplicate or missing connections—use Signal.get_connections():
func debug_signal_connections() -> void:
var connections = player.health_changed.get_connections()
for conn in connections:
print("Connected to: ", conn["callable"], " flags: ", conn["flags"])
Each entry in the array is a dictionary with keys "signal", "callable", and "flags". This lets you confirm exactly which callables are registered and spot duplicates or stale connections without needing to run the full game.
Related Issues
Signal-related crashes often appear alongside other lifecycle bugs. See Fix: Godot @export Variable Not Saving Between Sessions if your signal handlers are modifying state that isn’t persisting, and Fix: Godot NavigationAgent3D Not Reaching Target Position for issues where signals from navigation nodes fire at unexpected times.
Store your lambdas, or let CONNECT_ONE_SHOT do the cleanup for you.