oracle/graal · error · IllegalStateException

Reading handle from a closed scope.

Error message

Reading handle from a closed scope.

What it means

LibGraalObjectHandleScope wraps an object handle allocated from LibGraalObjectHandles; close() removes the handle and zeroes the field. getHandle() throws IllegalStateException('Reading handle from a closed scope.') when the handle is 0, i.e. the scope was already closed (or never initialized with a valid handle). This prevents use of a dangling object-handle registration after it was removed.

Source

Thrown at compiler/src/jdk.graal.compiler.libgraal/src/jdk/graal/compiler/libgraal/truffle/LibGraalObjectHandleScope.java:45

import java.io.Closeable;

final class LibGraalObjectHandleScope implements Closeable {

    private long handle;

    private LibGraalObjectHandleScope(long handle) {
        this.handle = handle;
    }

    @Override
    public void close() {
        LibGraalObjectHandles.remove(handle);
        handle = 0;
    }

    long getHandle() {
        if (handle == 0) {
            throw new IllegalStateException("Reading handle from a closed scope.");
        }
        return handle;
    }

    static LibGraalObjectHandleScope forObject(Object object) {
        return new LibGraalObjectHandleScope(LibGraalObjectHandles.create(object));
    }
}

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Keep handle usage strictly inside the scope's lifetime (try-with-resources body).
  2. Null out references when closing so later users fail with a clear NPE instead of reaching the closed scope.
  3. Reorder cleanup so consumers finish reading the handle before close().
  4. On IllegalStateException, re-acquire a fresh scope via LibGraalObjectHandleScope.forObject(obj) if the object is still live.

Example fix

// before
try (var scope = LibGraalObjectHandleScope.forObject(obj)) {
    queued = () -> scope.getHandle();
}
queued.run(); // scope already closed
// after (use the handle inside the scope, or re-scope later)
try (var scope = LibGraalObjectHandleScope.forObject(obj)) {
    long h = scope.getHandle();
    queued = () -> use(h);
}
Defensive patterns

Strategy: validation

Validate before calling

// Consume the handle strictly inside the scope's lifetime
try (LibGraalObjectHandleScope scope = LibGraalObjectHandleScope.forObject(obj)) {
    long h = scope.getHandle(); // OK here
    use(h);
}

Try / catch

try {
    long h = scope.getHandle();
} catch (IllegalStateException e) {
    if ("Reading handle from a closed scope.".equals(e.getMessage())) {
        try (LibGraalObjectHandleScope fresh = LibGraalObjectHandleScope.forObject(obj)) {
            h = fresh.getHandle();
        }
    }
}

Prevention

When it happens

Trigger: Calling getHandle() (directly or via a subclass like a TruffleObjectHandleScope used in try-with-resources) after close() ran — e.g. escaping the scope's lifetime, or reading the handle in a finally block after the resource closed.

Common situations: A handle object leaked past its try-with-resources block and used in a later callback; nested scopes closed in the wrong order; error paths that close early and then attempt one more interop call.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/dafb6409020d20b5. Report an issue: GitHub.