Quick answer: Godot 4 is strict about signal argument counts and types. If the emitter passes an int but the handler expects a float, or vice versa, the handler may silently not run while Godot logs a low-severity warning you probably never see. Declare signals with explicit types and match handler signatures exactly.
Here is how to fix Godot signals where emit_signal runs, no error appears, but the connected handler never executes. This is one of the most confusing bugs in Godot 4 because everything looks correct: the signal exists, the connection is made, the emit call fires. And still the function on the other end does absolutely nothing.
The Symptom
You have a signal declared on a node — something like a health component emitting damaged when it takes a hit. You connect a handler to it in another script. You call emit_signal("damaged", 10) and expect the handler to log or update UI.
Instead, nothing happens. You add a print to the emit site and confirm it runs. You add a print to the handler and confirm it does not. The connection is listed in the node inspector. There is no error in the output. Sometimes, if you look carefully at the Debugger panel with verbose logging enabled, you spot a line like Signal ‘damaged’: argument 0 expected Type X, got Type Y. This is the only clue.
In other cases the handler does fire, but only when you pass the value in a specific way — for example emit_signal("damaged", 10.0) works but emit_signal("damaged", 10) silently drops. The difference is a literal int versus a literal float, and Godot considers that a type error.
What Causes This
Godot 4 introduced typed signals. The syntax:
signal damaged(amount: int, source: Node)
declares the signal with enforced parameter types. When you connect a handler whose signature does not match — different parameter count, incompatible types, or a wrong-typed lambda — Godot refuses to dispatch to that handler. In debug builds you get a warning. In release builds the warning is suppressed.
The trap is that GDScript distinguishes between int and float strictly when types are declared. A handler typed func on_damaged(amount: float) -> void will not receive calls where the emitter passed an int. This matters because arithmetic in GDScript sometimes returns int (5 - 3 is int) and sometimes returns float (5.0 - 3 is float). If your signal expects int and you emit the result of a float expression, the call is dropped.
A second cause: connecting using the old Godot 3 string-based API with the wrong number of extra binds. connect("damaged", handler) works if handler takes exactly the signal’s parameters. But if your handler takes an additional context parameter, you must pass binds — and if the bind types do not match what the handler expects, Godot refuses the call.
A third cause: lambdas connected without types. A lambda like func(x): ... takes an untyped Variant, which is usually fine, but if the signal was declared with an unusual argument count, the lambda arity mismatch silently drops calls.
The Fix
Step 1: Declare signals with types. Always use the typed signal syntax in Godot 4:
extends Node
signal damaged(amount: int, source: Node)
signal healed(amount: float)
signal died()
func take_damage(amount: int, attacker: Node) -> void:
health -= amount
# New Godot 4 preferred emit syntax — no string name
damaged.emit(amount, attacker)
if health <= 0:
died.emit()
Notice the new emit syntax: signal_name.emit(args). This is type-checked at parse time against the signal declaration. Typos and arity mistakes become parse errors instead of runtime no-ops.
Step 2: Match handler signatures exactly.
func _ready() -> void:
# Good: handler signature matches signal
health_component.damaged.connect(_on_damaged)
# Good: typed lambda with matching types
health_component.healed.connect(func(amount: float) -> void:
print("Healed for ", amount))
func _on_damaged(amount: int, source: Node) -> void:
print("Took %d damage from %s" % [amount, source.name])
Step 3: Force int/float when emitting from ambiguous expressions. If you compute a value that might be int or float and your signal expects one specific type, cast explicitly:
var damage: int = int(base_damage * multiplier)
damaged.emit(damage, self)
Step 4: Enable verbose debug output to catch silent mismatches. In Project Settings under Debug, enable verbose_stdout. Godot will print warnings for every dropped signal call. Grep for the word “argument” in the output to surface these quickly during development.
If you are porting from Godot 3, replace everyemit_signal("name", args)withname.emit(args). The compiler will catch every signature mismatch immediately.
Why This Works
Typed signals turn runtime type errors into compile-time errors. The old string-based signal API cannot validate argument types because signals were effectively Variants on both ends. The new syntax makes the signal a first-class symbol with a known signature, and the parser enforces that emits and connects agree.
GDScript’s type system is nominally dynamic but gets stricter the more types you declare. When every signal is typed and every handler is typed, mismatches become impossible to introduce accidentally. When types are absent, GDScript falls back to Variant behavior where int and float are interconvertible — which is what masked this bug in Godot 3.
Related Issues
If your signals fire multiple times per event instead of once, see Fix: Godot Signal Connected Multiple Times. Double connections are a common cause of doubled game logic.
For signals that work in the editor but not in exported builds, see Fix: Godot Exported Build Signals Not Firing — usually a script export include issue.
Type your signals. Type your handlers. Usesignal.emit(). The parser catches every mistake.