Quick answer: Cyclic references occur when two or more scripts depend on each other in a circular chain. For example, script A preloads script B, and script B preloads script A. Since preload resolves at compile time, the engine cannot determine which script to compile first and reports a cyclic dependency error.
Here is how to fix Godot resource preload error cyclic. Your project was working fine until you added a preload() to a script, and now Godot refuses to compile anything. The error says “Cyclic reference” or “Could not load resource” and points to two or more scripts that depend on each other. This is one of the most structurally frustrating errors in GDScript because the fix requires understanding the difference between compile-time and runtime resource loading.
The Symptom
When opening your project or saving a script, Godot displays an error like “Parser Error: Could not fully preload the script, possible cyclic reference” or “Cyclic class reference”. The error may point to a specific preload() line, or it may point to a class_name declaration. Multiple scripts may show errors simultaneously, and the project may fail to run entirely.
This error is absolute — it prevents the affected scripts from compiling, which cascades to any script that depends on them. Your game will not run until the cycle is broken. Unlike runtime errors that you can sometimes work around, cyclic compile-time dependencies completely block the build.
A common scenario where this arises is when building an inventory system: the Inventory class references Item, and the Item class references Inventory (perhaps to call a method when the item is used). Or an Enemy script preloads a Bullet scene, and the Bullet script preloads the Enemy script to access its health or deal damage.
The error can also appear without explicit preload() calls. If two scripts both have class_name declarations and use each other as type hints (e.g., var enemy: Enemy in a Bullet script and var bullet: Bullet in an Enemy script), the engine must compile both to resolve the types, creating the same cyclic dependency.
How preload Creates Dependencies
Understanding why this happens requires knowing what preload() does differently from load():
preload() is a compile-time directive. When the GDScript compiler encounters preload("res://some_script.gd"), it immediately loads and compiles that script as part of compiling the current script. The preloaded resource is embedded as a constant in the compiled bytecode. This means the preloaded script must be fully compiled before the current script can finish compiling.
load() is a runtime function. When the game runs and reaches a load("res://some_script.gd") call, it loads the resource at that moment. The compiler does not need to resolve the reference during compilation, so no compile-time dependency is created.
# CREATES a compile-time dependency (resolved during script compilation)
const BulletScene = preload("res://bullet.tscn")
# Does NOT create a compile-time dependency (resolved at runtime)
var BulletScene = load("res://bullet.tscn")
The cyclic dependency happens when Script A preloads Script B, and Script B preloads Script A (directly or through a chain of other scripts). The compiler tries to compile A, sees it needs B, starts compiling B, sees it needs A, and realizes A is not finished compiling yet. It cannot proceed in either direction, so it reports the error.
This is the same problem as circular imports in Python, mutual includes in C++, or circular dependencies in any compiled language. GDScript’s preload creates the same kind of hard compile-time dependency.
Breaking the Cycle with load()
The simplest fix is to replace one of the preload() calls in the cycle with load(). Choose the direction that is less performance-critical:
# Enemy.gd - preloads bullet (this is fine)
extends CharacterBody2D
class_name Enemy
const BulletScene = preload("res://bullet.tscn")
func shoot():
var bullet = BulletScene.instantiate()
get_parent().add_child(bullet)
# Bullet.gd - uses load() instead of preload() to break the cycle
extends Area2D
func _on_body_entered(body: Node2D):
# Use is_in_group or duck typing instead of class reference
if body.is_in_group("enemies"):
body.take_damage(10)
queue_free()
In this example, the Bullet no longer needs to reference the Enemy class at all. Instead of checking if body is Enemy (which requires the Enemy class to be resolved), it uses is_in_group("enemies"), which works with any node regardless of its script type. This completely eliminates the dependency.
If you truly need to reference the other class (for example, to access specific typed properties), use load() at runtime:
# Bullet.gd - loads Enemy script at runtime when needed
extends Area2D
func _on_body_entered(body: Node2D):
var EnemyClass = load("res://enemy.gd")
if body is EnemyClass:
body.take_damage(10)
queue_free()
The performance impact of load() is minimal for scripts because Godot caches loaded resources. The first call loads from disk; subsequent calls return the cached version. If the load happens in a frequently called function (like _process), you can cache the result in a variable initialized in _ready().
class_name and Implicit Dependencies
The class_name keyword in GDScript registers a class globally so it can be used as a type hint anywhere in the project. This is convenient but creates implicit dependencies that can form cycles even without explicit preload() calls.
# Inventory.gd
class_name Inventory
extends Node
var items: Array[Item] = [] # References Item class_name
func add_item(item: Item):
items.append(item)
# Item.gd
class_name Item
extends Resource
var inventory: Inventory # References Inventory class_name - CYCLE!
func use():
inventory.remove_item(self)
In this example, neither script uses preload(), but using each other’s class_name as a type hint creates the same cyclic dependency. The compiler must resolve both class names, which means both scripts must be compiled, which is impossible because each waits for the other.
The fix is to break the type hint cycle by using a more general type on one side:
# Item.gd - use Node instead of Inventory to break the cycle
class_name Item
extends Resource
var inventory: Node # Generic type - no dependency on Inventory class
func use():
if inventory.has_method("remove_item"):
inventory.remove_item(self)
This uses duck typing (has_method) instead of the specific class reference. You lose static type checking on the inventory variable, but you eliminate the cyclic dependency. In practice, the duck typing check catches the same errors at runtime that the type system would catch at compile time.
Architectural Patterns to Avoid Cycles
The best long-term solution is to structure your code so that dependencies flow in one direction. Here are patterns that prevent cycles from forming:
Signal bus pattern. Instead of two classes referencing each other directly, both reference a shared autoload that acts as a signal bus. The Bullet emits a signal to the bus, and the Enemy listens to the bus. Neither class needs to know about the other.
# EventBus.gd (Autoload)
extends Node
signal damage_dealt(target: Node, amount: int)
# Bullet.gd - emits to the bus, no Enemy dependency
extends Area2D
func _on_body_entered(body: Node2D):
EventBus.damage_dealt.emit(body, 10)
queue_free()
# Enemy.gd - listens to the bus, no Bullet dependency
extends CharacterBody2D
func _ready():
EventBus.damage_dealt.connect(_on_damage_dealt)
func _on_damage_dealt(target: Node, amount: int):
if target == self:
health -= amount
Base class extraction. If two classes share behavior, extract the common interface into a base class that both extend. The base class has no dependencies on either child, and the children only depend on the base.
resource_local_to_scene. For Resource subclasses that reference other resources, enabling resource_local_to_scene creates unique copies of the resource per scene instance. This does not directly fix cyclic references, but it avoids shared resource state that often motivates bidirectional references in the first place.
# Make a resource unique per scene instance
class_name ItemData
extends Resource
@export var item_name: String
@export var quantity: int = 1
func _init():
resource_local_to_scene = true
When you find yourself creating a cyclic dependency, it is usually a sign that the two classes are too tightly coupled. Step back and ask whether there is a third entity (a manager, a bus, a base class) that should mediate the interaction. The extra indirection is a small price for code that compiles and scales.
"Cyclic dependencies are an architecture problem, not a language problem. If two things need each other, they are really one thing, or there is a missing third thing that both should depend on instead."
Related Issues
If your scripts compile but exported resources are null at runtime, see export variable resource null at runtime. For class_name conflicts that produce different errors, check class_name cyclic reference errors which covers the registration system in detail. If your preload errors only happen in exported builds but not in the editor, missing resources in exports explains how the export process handles resource paths differently.
Replace one preload with load. That breaks the cycle.