{"record":{"id":"2990ce7f2350b161","repo":"oracle/graal","slug":"could-not-detach-thread-correctly","errorCode":null,"errorMessage":"Could not detach thread correctly","messagePattern":"Could not detach thread correctly","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/EspressoContext.java","lineNumber":1077,"sourceCode":"    }\n\n    public void disposeThread(Thread hostThread) {\n        StaticObject guestThread = getGuestThreadFromHost(hostThread);\n        if (guestThread == null) {\n            return;\n        }\n        try {\n            // Cannot run guest code after finalizeContext was called (GR-35712).\n            if (isFinalized()) {\n                return;\n            }\n            if (hostThread != Thread.currentThread()) {\n                String guestName = threads.getThreadName(guestThread);\n                getLogger().warning(\"unimplemented: disposeThread for non-current thread: \" + hostThread + \" / \" + guestName + \". Called from thread: \" + Thread.currentThread());\n                return;\n            }\n            if (vm.DetachCurrentThread(this, getLanguage()) != JNI_OK) {\n                throw new RuntimeException(\"Could not detach thread correctly\");\n            }\n        } finally {\n            unregisterThread(guestThread);\n        }\n    }\n\n    public StaticObject getGuestThreadFromHost(Thread host) {\n        return espressoEnv.getThreadRegistry().getGuestThreadFromHost(host);\n    }\n\n    public void registerCurrentThread(StaticObject guestThread) {\n        getLanguage().getThreadLocalState().initializeCurrentThread(guestThread);\n    }\n\n    public StaticObject getCurrentPlatformThread() {\n        return getLanguage().getThreadLocalState().getCurrentPlatformThread(this);\n    }\n","sourceCodeStart":1059,"sourceCodeEnd":1095,"githubUrl":"https://github.com/oracle/graal/blob/a66e9ccd1d7bf2552883939aa0788dfd0e294aab/espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/runtime/EspressoContext.java#L1059-L1095","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Ensure the thread that disposes is the same host thread that attached: call disposeThread from within the attached thread before it exits.","Detach/attach guest threads symmetrically: pair every attach with exactly one dispose, and do not dispose twice.","Avoid closing the polyglot Context while guest threads are still running guest code; join guest threads first, then close.","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."],"exampleFix":"// before (wrong thread disposes)\nThread t = Thread.attach(context);\n// ... later, from main thread:\ncontext.disposeThread(t); // warns and skips, or detach races\n\n// after (thread disposes itself)\ntry {\n    // work on guest thread\n} finally {\n    context.disposeThread(t); // same host thread\n}","handlingStrategy":"try-catch","validationCode":"// Before disposing, confirm same host thread to avoid the detach path entirely\nif (thread == Thread.currentThread() && !context.isFinalized()) {\n    context.disposeThread(thread);\n}","typeGuard":null,"tryCatchPattern":"try {\n    context.disposeThread(thread);\n} catch (RuntimeException e) {\n    if (e.getMessage().contains(\"Could not detach thread\")) {\n        // log and continue; thread is already unregistered in finally\n    } else {\n        throw e;\n    }\n}","preventionTips":["Dispose guest threads from the thread itself before it exits.","Join all guest threads before closing the Context.","Pair every attach with exactly one dispose; never dispose twice."],"tags":["espresso","jni","threading","lifecycle"],"backgroundTag":null,"analyzedSha":"a66e9ccd1d7bf2552883939aa0788dfd0e294aab","analyzedAt":"2026-08-14T13:58:47.161Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}