Trying to understand order of initialization (ExceptionInInitializerError)

The following code throws an ExceptionInInitializerError. After some brief investigation, I’ve come to the understanding that companion object fields are instantiated lazily (according to this SO post).

So it seems that when println(Foo.Bar) is called, object Foo is initialized. As part of its initialization, foos2 is initialized. foos2’s initializer filters foos1, but Bar in foos1 turns out to be null, which leads to a NullPointerException being thrown, and thus an ExceptionInInitializerError.

Is there a way to make Bar be initialized before everything else? What’s the best way to stop the ExceptionInInitializerError?

Scastie link

sealed abstract class Foo(
    val value: Int,
    val opt: Option[Int] = None // removing this field stops the NullPointerException
) {}

object Foo {
  case object Bar extends Foo(42) // throws ExceptionInInitializerError

  val foos1 = List(Bar)

  val foos2: List[Foo] = {
    println(foos1) // prints List(null)
    val result = foos1.filter(_.value == 42) // throws NullPointerException
    result
  }
}

object Repro {
  def main(args: Array[String]) = {
    println(Foo.Bar)
  }
}

You can use lazy val to postpone the initialization of foos until accessed:

1 Like

The problem is that Bar uses the default arg for the parameter to Foo, and that default arg is a method of the companion object Foo.

Specify the arg instead:

case object Bar extends Foo(42, None)

If the default arg were implemented as a static method of Foo, there would be no circular dependency.

In the same vein, Scala 2 fails slightly differently from Scala 3 because of how the lambda used by foos2 is encoded.

In other words, these initialization snafus are a combination of language-level definitions and implementation details.

To clarify, the linked SO only says, correctly, that object Foo and object Bar are lazy, not that their fields are lazy. Making everything lazy is also a helpful approach, see the other reply.

The SO question offers an interesting perspective: at the JVM class level (or Java language level), static initialization runs first. If the companion object models statics in Java, it’s natural to expect “companion initialization” first. Instead, the companion is a different class entirely (Foo$ as opposed to Foo). The runtime behavior depends, in part, on how the machinery is compiled (that is, to which class, and whether it is static).

In Scala 3, -Ysafe-init doesn’t help here, but -Ysafe-init-global does the trick:

➜  scalac -Ysafe-init-global companion-init.scala
-- Warning: companion-init.scala:7:14 ----------------------------------------------------------------------------------
7 |  case object Bar extends Foo(42) // throws ExceptionInInitializerError
  |              ^
  |              Cyclic initialization: object Bar -> object Foo -> object Bar. Calling trace:
  |              ├── case object Bar extends Foo(42) // throws ExceptionInInitializerError      [ companion-init.scala:7 ]
  |              │                           ^
  |              ├── object Foo {       [ companion-init.scala:6 ]
  |              │   ^
  |              └── val foos1 = List(Bar)      [ companion-init.scala:9 ]
  |                                   ^^^
1 warning found
4 Likes

I feel that another “Break the candidate” interview question has been born! :japanese_goblin:

So tell me what you think this code does…

1 Like