Skip to content
Cyclic reference involving ...

Cyclic reference involving ...

DodaTech 3 min read

The “Cyclic reference” error in Scala occurs when two objects or vals depend on each other during initialisation, preventing safe evaluation order.

What It Means

Scala initialises objects and vals eagerly by default. When object A references object B and object B references object A at initialisation time, the compiler detects the cycle and reports “Cyclic reference involving …”. This can also happen with val initialisation loops within the same class or across companion objects, where the compiler cannot determine a valid evaluation order.

Why It Happens

  • Two singleton objects reference each other at construction time.
  • A class and its companion object have circular val dependencies.
  • A sealed trait hierarchy has a circular type reference between its subtypes.
  • Lazy evaluation is needed but val (eager) was used instead of lazy val.
  • Two case classes contain fields of each other’s types without using Option or indirection.
  • A val initialiser references itself (e.g., val x: Int = x + 1).

How to Fix It

1. Use lazy val to break the cycle

// ❌ Cyclic reference involving A and B
object A {
  val bValue: Int = B.value
}
object B {
  val value: Int = A.bValue + 1
}

// ✅ Use lazy val to defer initialisation
object A {
  lazy val bValue: Int = B.value // computed on first access
}
object B {
  lazy val value: Int = A.bValue + 1
}

2. Restructure the dependency direction

// ❌ Circular dependency
class User(name: String) {
  val profile: Profile = new Profile(this)
}
class Profile(user: User) {
  val displayName: String = user.name
}

// ✅ Break the cycle with a shared data object
case class UserData(name: String, email: String)
class User(data: UserData) {
  val profile: Profile = new Profile(data)
}
class Profile(data: UserData) {
  val displayName: String = data.name
}

3. Use sealed trait based design for type cycles

// ❌ Cyclic reference in type aliases
trait A { type B }
// Sometimes cycles can appear with complex type members

// ✅ Restructure to avoid mutual type references
trait Base
trait A extends Base
trait B extends Base

4. Use Option for self-referencing structures

// ❌ Cyclic reference (case class cannot contain itself directly)
case class Node(name: String, parent: Node)

// ✅ Use Option to break the cycle
case class Node(name: String, parent: Option[Node])
val root = Node("root", None)
val child = Node("child", Some(root))

5. Extract shared state into a separate object

// ❌ Two objects with mutual references
object Config {
  val timeout: Int = Service.defaultTimeout
}
object Service {
  val defaultTimeout: Int = Config.timeout
}

// ✅ Extract the shared constant
object Defaults {
  val defaultTimeout: Int = 5000
}
object Config {
  val timeout: Int = Defaults.defaultTimeout
}
object Service {
  val defaultTimeout: Int = Defaults.defaultTimeout
}
Why doesn’t Scala allow circular references by default?
Eager initialisation of val and object in Scala follows a deterministic order. Circular references at initialisation time cannot be resolved without runtime overhead or non-deterministic behaviour. lazy val defers evaluation until first access, breaking the initialisation cycle.
Can cyclic references happen with def?
No — def creates a method that is evaluated each time it’s called, not once at initialisation. Methods can call each other circularly without issues. The cycle only matters for val, lazy val, and object initialisation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro