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.