Quick answer: Scene unique names (the % syntax) only resolve within the same scene owner. If your node was added at runtime without calling set_owner(), or if it belongs to a different sub-scene, the lookup will return null. Verify ownership and ensure the unique name flag is actually set on the target node.
Godot 4 introduced scene unique names as a way to reference nodes without brittle, hard-coded paths. Instead of writing $UI/HUD/HealthBar/Label, you mark the Label as unique and write %HealthLabel. It’s a great feature — until it silently returns null and your game crashes with no useful error message. Here’s what’s going wrong and how to fix it.
Understanding the % Syntax and Scene Ownership
The % prefix in GDScript is shorthand for accessing a scene unique name. When you write %PlayerSprite, Godot does not search the entire scene tree. It searches only among nodes that share the same scene owner as the script’s node. This is the critical detail that most developers miss.
Every node in Godot has an owner property. When you create a scene in the editor and save it, the root of that scene becomes the owner of all its children. This ownership relationship is what defines the boundary of unique name resolution. If a node’s owner is a different scene root, it is invisible to your % lookup.
# This works when HealthLabel is owned by the same scene
func _ready():
var label = %HealthLabel
label.text = "100"
# This fails silently if HealthLabel is in a sub-scene
func _ready():
var label = %HealthLabel # Returns null
label.text = "100" # Crash: calling method on null
Common Causes of Unique Name Resolution Failure
The node is in a different sub-scene. This is by far the most common cause. If you have a Player scene that instances a HUD scene, a script on the Player cannot use % to access nodes inside the HUD scene. The HUD’s nodes are owned by the HUD scene root, not the Player scene root. You need to use a regular node path or expose the node through a method on the HUD script.
The node was added at runtime without setting its owner. When you instantiate a node with Node.new() or load a packed scene and add it as a child, the new node’s owner is not automatically set. You must call set_owner() explicitly if you want it to participate in unique name resolution.
# Adding a node at runtime — owner must be set manually
func add_dynamic_label():
var label = Label.new()
label.name = "DynamicLabel"
label.unique_name_in_owner = true
add_child(label)
label.owner = get_tree().current_scene # Required for % to work
The unique name flag was never set. Simply naming a node something descriptive does not make it a unique name. You must explicitly enable it by right-clicking the node in the Scene dock and selecting Access as Unique Name, or by setting unique_name_in_owner = true in code. Look for the % icon next to the node name in the editor to confirm it is active.
Duplicate unique names in the same scene. If two nodes in the same scene have the same unique name, Godot will raise an error. Check your scene for accidentally duplicated nodes, especially after copy-paste operations.
Debugging Unique Name Issues
When %NodeName returns null, the error message is not always helpful. Here is a systematic approach to diagnosing the problem.
# Step 1: Verify the node exists in the tree at all
func _ready():
var node = get_node_or_null("HealthLabel")
print("By name: ", node)
# Step 2: Check what this node's owner is
print("My owner: ", owner)
print("My owner name: ", owner.name if owner else "null")
# Step 3: If the target node exists, check its owner
if node:
print("Target owner: ", node.owner)
print("Target unique: ", node.unique_name_in_owner)
# Step 4: Print the tree to see the full hierarchy
print_tree_pretty()
The key question is whether your node’s owner and the target node’s owner are the same object. If they differ, % will never find the target. You either need to restructure your scenes so they share ownership, or fall back to a regular node path with get_node().
Best Practices for Scene Unique Names
Use unique names for nodes that are stable parts of a scene’s internal structure — UI elements, spawn points, camera rigs. Do not rely on them for nodes that cross scene boundaries or are created dynamically. For cross-scene communication, use signals, groups, or direct references passed through exported variables.
When working with inherited scenes, be aware that unique names set in a base scene are inherited by child scenes. Overriding a node in a child scene preserves the unique name flag, but removing and re-adding a node may lose it. Always verify after structural changes to inherited scenes.
If your project uses a lot of runtime node creation, consider building a simple node registry instead of relying on unique names. A dictionary on an autoload singleton gives you global name-based lookup without the ownership constraints.
# Autoload singleton: NodeRegistry.gd
var _registry: Dictionary = {}
func register(key: String, node: Node):
_registry[key] = node
func get_registered(key: String) -> Node:
return _registry.get(key)
func unregister(key: String):
_registry.erase(key)
Check the owner, check the flag, check the scene boundary.