Skip to content

GWT class initializers don't fail in the same way as Java class initializers #10133

@niloc132

Description

@niloc132

As far as I'm aware, this isn't a regression of any kind, but has always been this way. Likewise, I don't have a concrete plan/need to change it, but want to have it documented for future reference.

In the JVM, if a static initializer throws (either a static block in a class, or the initializer expression for a static field declaration), that exception is wrapped with a java.lang.ExceptionInInitializerError. Later uses of that class will instead throw a java.lang.NoClassDefFoundError, wrapping an ExceptionInInitializerError, without the original exception.

The present behavior in GWT is to throw the actual exception that occurred, without wrapping in ExceptionInInitializerError when the class initialization takes place and fails. Then, later uses of the class (creating instances, access to static members) will succeed despite the partially initialized state.

Example unit test demonstrating GWT's current behavior

  public static class HasFailingClinit {
    static boolean startedClinit;
    static boolean finishedClinit;
    static {
      startedClinit = true;
      if (true) {
        throw new IllegalStateException("clinit failed");
      }
    }
    static {
      finishedClinit = true;
    }
  }

  public void testFailingClinit() {
    // GWT doesn't handle class initializers that fail in the same way as the JVM - whereas
    // plain java would throw ExceptionInInitializerError on every usage of the class, GWT
    // will only throw the first exception it encounters, then permit the class to be used,
    // uninitialized.
    // First access to any field will throw:
    try {
      boolean ignored1 = HasFailingClinit.startedClinit;
      fail("Expected IllegalStateException");
    } catch (IllegalStateException ignored2) {
      // expected
    }
    // Second access will succeed, and expose the half-initialized state
    assertTrue(HasFailingClinit.startedClinit);
    assertFalse(HasFailingClinit.finishedClinit);
  }

Presently, class initializers are implemented roughly like this (using the above test as an example), merging all initializers into a single function:

function HasFailingClinit_clinit() {
  HasFailingClinit_clinit = function(){};
  startedClinit = true;
  throw new IllegalStateException("clinit failed");
  finishedClinit = true;
}

The best solution would be to wrap the contents of the function with a try/catch, and re-assign the function to something that throws on failure:

function HasFailingClinit_clinit() {
  HasFailingClinit_clinit = function(){};
  try {
    startedClinit = true;
    throw new IllegalStateException("clinit failed");
    finishedClinit = true;
  } catch (ex) {
    HasFailingClinit_clinit = function(){throw new ExceptionInInitializerError()};
    throw new ExceptionInInitializerError(ex);
  }
}

This would add a decent amount of overhead per class, and only matters for the case where an initializer would be expected to throw, so I'm inclined to let this difference remain.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions