Quick answer: Store the Callable in a variable. Use the same variable for both connect and disconnect. Lambdas and .bind() calls return fresh Callables each time.
A scene exits cleanly until you see: Disconnecting nonexistent signal ‘body_entered’. The connect succeeded; the disconnect can’t find the entry. The Callable used for disconnect is a different instance than the one connected.
Callable Identity
Each Callable.new() or method.bind(arg) produces a unique object. Two Callables wrapping the same method are different instances; Godot’s disconnect compares by identity, so they don’t match.
The Fix
var _enter_cb: Callable
func _ready():
_enter_cb = _on_body_entered
area.body_entered.connect(_enter_cb)
func _exit_tree():
area.body_entered.disconnect(_enter_cb)
Stored Callable; same instance for both. Disconnect finds the entry; no error.
Lambdas
# Wrong: each call creates a new Callable
area.body_entered.connect(func(body): handle(body))
# Later:
area.body_entered.disconnect(func(body): handle(body)) # different Callable
# Right:
var cb = func(body): handle(body)
area.body_entered.connect(cb)
area.body_entered.disconnect(cb)
Bind Arguments
# Same Callable, used for both ends
var cb = _on_pickup.bind(pickup_id)
area.body_entered.connect(cb)
# Later:
area.body_entered.disconnect(cb)
Store the bound Callable; reuse for disconnect.
Check Before Disconnect
if area.body_entered.is_connected(_enter_cb):
area.body_entered.disconnect(_enter_cb)
Defensive check. Avoids errors if disconnect is called twice or the connect never fired.
Verifying
Re-trigger scene exits. No more disconnect errors. Use the Debugger’s Output Log to confirm clean exits.
“Same Callable for connect and disconnect. Store it; don’t recreate. Lambdas and binds create fresh instances each call.”
When using bind for argument passing, the storage pattern is essential — binds create new Callables every call site.