Five Godot 4 bugs that never throw an error
The bugs that cost me the most time in Godot weren't the crashes. They were the ones where everything runs, nothing turns red, and the result is just wrong. Here are five, with the fix for each.
1. Appending to a packed array inside a Dictionary does nothing
var hits := {"knight": PackedInt32Array()}
(hits["knight"] as PackedInt32Array).append(7)
print(hits["knight"]) # [] - the append went to a copy
Packed arrays are values, not references. They're copy-on-write: what you get out of the Dictionary shares memory with the original only until you change it, and then it's a separate copy. The Dictionary never sees the change.
Fix: take it out, change it, put it back.
var list: PackedInt32Array = hits["knight"]
list.append(7)
hits["knight"] = list
A plain Array is a reference, so this only bites with the Packed*Array types.
2. Lambdas capture local variables by value
var count := 0
var bump := func(): count += 1
bump.call()
bump.call()
print(count) # 0
A lambda gets its own copy of every local variable it uses, taken when the lambda is created. count += 1 changes the lambda's copy, never the outer count. Godot 4.3 and later show a warning for this one, if you read your warnings.
Fix: capture something that's a reference, and change what's inside it.
var count := [0]
var bump := func(): count[0] += 1
The same rule goes the other way: if you later assign a new Array to count, the lambda keeps the old one.
3. A helper method named _set or _get
_set(property, value) and _get(property) are virtual methods on Object, the hooks behind custom properties. Name a helper _set with two arguments, and Godot treats it as the override: it now runs on property assignments to that object, including the ones the engine makes itself when it loads the scene.
Fix: never use those two names for anything else. store(), put() and read() are all free.
4. Bone poses from a SkeletonModifier3D look like they never happened
You write an IK solver, a look-at or a ragdoll as a SkeletonModifier3D, it clearly works on screen, and then get_bone_global_pose() in _process returns the pose from before your modifier ran.
Fix: read modified poses inside the skeleton's skeleton_updated signal. That's the point in the frame where the final, modified pose exists.
5. Skinned meshes vanish at the edge of the screen
A character's arm, a pair of wings, or a muscle blend shape blinks out while it's still clearly on screen, usually near the edges. The culling box Godot uses for a skinned mesh doesn't account for blend shapes, so a part that a blend shape pushes outward can stick out of its own box and get culled while you can still see it.
Fix: give those meshes an extra_cull_margin on the MeshInstance3D (0.6 m was enough for my blend-shaped body parts), or set a custom_aabb big enough for the largest shape.
None of these are engine bugs exactly. Most are documented somewhere. But none of them tell you anything when they happen, and that's what makes them expensive.