Quick answer: The most common cause is using Godot 3 signal connection syntax in Godot 4. In Godot 4, use signal_name.connect(callable) instead of connect('signal_name', target, 'method_name'). Also ensure the signal is declared with the signal keyword and emitted with signal_name.
Here is how to fix Godot signals not connecting. You declared a custom signal, connected it, emitted it — and your handler function is never called. No errors, no warnings, just silence. Godot 4 fundamentally changed how signals are connected and emitted compared to Godot 3, and mixing old and new syntax is the most common cause of signals that silently fail to connect.
The Symptom
You have a signal declared on one node and a handler function on another (or the same) node. When the signal is emitted, the handler does not execute. There are no error messages about the connection failing, and no indication that anything is wrong. The signal simply does not reach its destination.
This is different from signals that produce an error on emission, which typically means a parameter mismatch. Silent failure means the connection itself was never established, or it was established incorrectly and the signal goes to a method that does not exist or is on a freed node.
A variant of this problem is signals that worked in Godot 3 but stopped working after migrating to Godot 4. The signal declaration and emission syntax changed significantly between versions, and the migration tool does not always convert everything correctly.
Another variant involves signals connected in the editor via the Node dock that break after renaming the handler function or the node. Editor connections are stored by function name string, so renaming either side silently breaks the connection.
Godot 4 Signal Syntax
The most important change in Godot 4 is that signals are now first-class objects. In Godot 3, signals were connected and emitted using string-based APIs. In Godot 4, they use type-safe callable syntax:
# Godot 3 syntax - DOES NOT WORK in Godot 4
connect("my_signal", target_node, "_on_my_signal")
emit_signal("my_signal", arg1, arg2)
# Godot 4 syntax - CORRECT
my_signal.connect(target_node._on_my_signal)
my_signal.emit(arg1, arg2)
The old connect() and emit_signal() methods still exist in Godot 4 for compatibility, but they behave differently. The string-based connect("signal_name", target, "method") three-argument form was removed. If you try to use it, you will get a parser error or unexpected behavior depending on what arguments you pass.
Here is the complete pattern for declaring, connecting, and emitting a custom signal in Godot 4:
# Emitter.gd - the node that sends the signal
extends Node2D
signal health_changed(new_health: int)
signal died
var health: int = 100
func take_damage(amount: int):
health -= amount
health_changed.emit(health)
if health <= 0:
died.emit()
# Receiver.gd - the node that listens for the signal
extends Control
@onready var player = $"../Player"
@onready var health_label = $HealthLabel
func _ready():
player.health_changed.connect(_on_health_changed)
player.died.connect(_on_player_died)
func _on_health_changed(new_health: int):
health_label.text = "HP: " + str(new_health)
func _on_player_died():
health_label.text = "DEAD"
Notice that the connection uses a direct method reference (_on_health_changed) rather than a string. This is a callable in Godot 4 terms. The engine validates the connection at the point of calling connect(), so if the method does not exist, you get an immediate error rather than a silent failure at emission time.
Parameter Mismatches
When a signal is emitted with arguments, the connected function must accept the same number of arguments. If there is a mismatch, Godot 4 prints an error to the Output panel but does not crash. This error is easy to miss.
# Signal declaration with two parameters
signal item_collected(item_name: String, value: int)
# WRONG: Handler expects one parameter but signal sends two
func _on_item_collected(item_name: String):
print(item_name) # Error: too many arguments
# CORRECT: Handler matches signal parameters
func _on_item_collected(item_name: String, value: int):
print(item_name, " worth ", value)
# ALSO CORRECT: Use fewer params if you don't need them all
# (only works if you bind the connection differently)
If you want to ignore some signal parameters in the handler, you can use a lambda as a wrapper:
# Connect with a lambda to ignore the second parameter
item_collected.connect(func(item_name, _value): _on_item_name_only(item_name))
func _on_item_name_only(item_name: String):
print(item_name)
Lambda connections are powerful but come with a caveat: you cannot easily disconnect them later because you need a reference to the exact lambda callable. For signals that you might need to disconnect (like when a node leaves the scene), use named functions instead.
Connecting to the Right Node Instance
A subtle but common issue is connecting to the wrong node instance. If you have multiple instances of the same scene, connecting to the signal in one instance does not automatically connect to the same signal in other instances.
# Spawner.gd - connects to each spawned enemy
extends Node2D
var enemy_scene = preload("res://enemy.tscn")
func spawn_enemy():
var enemy = enemy_scene.instantiate()
add_child(enemy)
# Connect to THIS specific enemy's signal
enemy.died.connect(_on_enemy_died.bind(enemy))
func _on_enemy_died(enemy: Node2D):
print("Enemy died: ", enemy.name)
enemy.queue_free()
The bind() method attaches additional arguments to the callable. When the signal fires, the bound arguments are appended after the signal’s own arguments. This is essential when you need to know which specific emitter sent the signal — for example, tracking which enemy in a group just died.
Another issue arises when the emitting node is freed before the signal is disconnected. In Godot 4, if the emitting node is freed, all its signal connections are automatically cleaned up. But if the receiving node is freed while the emitter still exists, the emitter may try to call a method on a freed instance. To prevent this, disconnect signals in _exit_tree() or use the CONNECT_ONE_SHOT flag for signals that should only fire once.
# One-shot connection: auto-disconnects after first emission
my_signal.connect(_on_my_signal, CONNECT_ONE_SHOT)
# Manual disconnect in _exit_tree
func _exit_tree():
if player.health_changed.is_connected(_on_health_changed):
player.health_changed.disconnect(_on_health_changed)
Editor Connections vs Code Connections
Godot allows you to connect signals in two ways: through the editor’s Node dock (visual connection), or in code using connect(). Both approaches work, but they have different failure modes.
Editor connections are stored in the scene file as metadata. They reference the target node by path and the handler function by name. If you rename the function in your script, the editor connection silently breaks because the stored function name no longer matches. Godot does not warn you about this at edit time; you only discover the problem when the signal fails to fire at runtime.
Code connections are validated at runtime when connect() is called. If the callable is invalid (the method does not exist), Godot prints an error immediately. This makes code connections more robust because errors are caught at connection time rather than emission time.
For this reason, many experienced Godot developers prefer connecting all signals in code rather than using the editor. Code connections are also easier to track in version control and do not depend on the scene file being in a specific state.
# Prefer code connections for maintainability
func _ready():
$Button.pressed.connect(_on_button_pressed)
$Timer.timeout.connect(_on_timer_timeout)
$Area2D.body_entered.connect(_on_body_entered)
# Check for duplicate connections if _ready can be called multiple times
func _ready():
if not $Button.pressed.is_connected(_on_button_pressed):
$Button.pressed.connect(_on_button_pressed)
If you connect the same signal to the same callable twice, Godot prints a warning and ignores the duplicate. But if your _ready() runs multiple times (which can happen with scene tree changes), checking is_connected() first avoids the warning spam and makes your intent clear.
"Every signal bug I have seen in the last year was someone using Godot 3 syntax in a Godot 4 project. The callable-based connect is not just different syntax — it is a fundamentally better system that catches errors earlier."
Related Issues
If your signals connect but fire multiple times per event, see our guide on duplicate signal connections. For signals that work within a scene but fail across scene boundaries, cross-scene signal communication covers autoload-based signal buses. If your custom signal does not appear in the editor’s Node dock for visual connection, custom signal not showing in editor explains the requirements for editor visibility.
Use the Godot 4 callable syntax. Drop the strings.