oracle/graal · error · RuntimeException

Could not detach thread correctly

Error message

Could not detach thread correctly

What it means

Thrown by EspressoContext.disposeThread when the underlying JNI call DetachCurrentThread does not return JNI_OK. Espresso (Java-on-Truffle) attaches host threads to the guest VM; detaching releases guest-side thread state (JNI env, monitors, TLS). If the native detach fails, the runtime cannot guarantee a clean teardown, so it throws a RuntimeException after which the thread is still unregistered in a finally block.

Source

Thrown at espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/EspressoContext.java:1077

    }

    public void disposeThread(Thread hostThread) {
        StaticObject guestThread = getGuestThreadFromHost(hostThread);
        if (guestThread == null) {
            return;
        }
        try {
            // Cannot run guest code after finalizeContext was called (GR-35712).
            if (isFinalized()) {
                return;
            }
            if (hostThread != Thread.currentThread()) {
                String guestName = threads.getThreadName(guestThread);
                getLogger().warning("unimplemented: disposeThread for non-current thread: " + hostThread + " / " + guestName + ". Called from thread: " + Thread.currentThread());
                return;
            }
            if (vm.DetachCurrentThread(this, getLanguage()) != JNI_OK) {
                throw new RuntimeException("Could not detach thread correctly");
            }
        } finally {
            unregisterThread(guestThread);
        }
    }

    public StaticObject getGuestThreadFromHost(Thread host) {
        return espressoEnv.getThreadRegistry().getGuestThreadFromHost(host);
    }

    public void registerCurrentThread(StaticObject guestThread) {
        getLanguage().getThreadLocalState().initializeCurrentThread(guestThread);
    }

    public StaticObject getCurrentPlatformThread() {
        return getLanguage().getThreadLocalState().getCurrentPlatformThread(this);
    }

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Ensure the thread that disposes is the same host thread that attached: call disposeThread from within the attached thread before it exits.
  2. Detach/attach guest threads symmetrically: pair every attach with exactly one dispose, and do not dispose twice.
  3. Avoid closing the polyglot Context while guest threads are still running guest code; join guest threads first, then close.
  4. If the failure reproduces, collect a repro and report it to the GraalVM Espresso project (https://github.com/oracle/graal/issues) since JNI_OK failure paths usually indicate a VM bug.

Example fix

// before (wrong thread disposes)
Thread t = Thread.attach(context);
// ... later, from main thread:
context.disposeThread(t); // warns and skips, or detach races

// after (thread disposes itself)
try {
    // work on guest thread
} finally {
    context.disposeThread(t); // same host thread
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before disposing, confirm same host thread to avoid the detach path entirely
if (thread == Thread.currentThread() && !context.isFinalized()) {
    context.disposeThread(thread);
}

Try / catch

try {
    context.disposeThread(thread);
} catch (RuntimeException e) {
    if (e.getMessage().contains("Could not detach thread")) {
        // log and continue; thread is already unregistered in finally
    } else {
        throw e;
    }
}

Prevention

When it happens

Trigger: Calling context.disposeThread() (or polyglot Context.close() / Engine cleanup that disposes attached threads) when the current thread's JNI detach fails: e.g. detach of a thread that still holds guest monitors, a thread attached via a different JNI version, or shutdown racing with finalizeContext/thread teardown.

Common situations: Polyglot embedding where user code attaches threads with Thread.attach(...) and closes contexts concurrently; teardown during context finalization (GR-35712 related races); running with native access restrictions that make DetachCurrentThread fail; calling disposeThread from a different thread (which is separately warned about, not thrown).

Related errors


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