{"record":{"id":"76f53994e4bd4088","repo":"hibernate/hibernate-orm","slug":"transaction-was-rolled-back-in-a-different-thread","errorCode":null,"errorMessage":"Transaction was rolled back in a different thread","messagePattern":"Transaction was rolled back in a different thread","errorType":"exception","errorClass":"HibernateException","httpStatus":null,"severity":"error","filePath":"hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jta/internal/synchronization/SynchronizationCallbackCoordinatorTrackingImpl.java","lineNumber":87,"sourceCode":"\t\tdoAfterCompletion( isCommitted( status ), false );\n\t}\n\n\t@Override\n\tpublic void synchronizationRegistered() {\n\t\tregistrationThreadId = Thread.currentThread().getId();\n\t}\n\n\t@Override\n\tpublic void processAnyDelayedAfterCompletion() {\n\t\tif ( delayedCompletionHandling ) {\n\t\t\tdelayedCompletionHandling = false;\n\n\t\t\t// false here (rather than how we used to keep and check the status) because as discussed above\n\t\t\t// the delayed logic should only ever occur during rollback\n\t\t\tdoAfterCompletion( false, true );\n\n\t\t\t// NOTE: doAfterCompletion calls reset\n\t\t\tthrow new HibernateException( \"Transaction was rolled back in a different thread\" );\n\t\t}\n\t}\n}\n","sourceCodeStart":69,"sourceCodeEnd":91,"githubUrl":"https://github.com/hibernate/hibernate-orm/blob/fad1729dce015f908198d57a8d80274a30f905a5/hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jta/internal/synchronization/SynchronizationCallbackCoordinatorTrackingImpl.java#L69-L91","documentation":"In JTA environments Hibernate registers a JTA Synchronization and tracks which thread owns the transaction/Session. When afterCompletion is signaled from a different thread, Hibernate cannot safely run it there, so it sets delayedCompletionHandling and defers. The next time the owning thread touches the coordinator, processAnyDelayedAfterCompletion() replays the after-completion work (doAfterCompletion, which resets state) and then throws this HibernateException to signal that the transaction finished on another thread.","triggerScenarios":"A JTA transaction is rolled back or completed by a non-owner thread — e.g. Narayana/Arjuna transaction-reaper timeout abort, asynchronous rollback in WildFly/WebSphere/Liberty, or handing the Session to an executor — and then the original thread performs any Session operation (find, flush, close, begin a new transaction) which drains the delayed completion.","commonSituations":"Transaction timeouts under Spring/EJB JTA where the reaper thread aborts the tx; sharing an EntityManager/Session across threads (CompletableFuture, @Async, custom executors); Quarkus/WildFly async request processing; test harnesses that roll back transactions on a background thread.","solutions":["Treat the Session/EntityManager as unusable: catch the exception, close/discard the session, and create a fresh one — never reuse it","Stop sharing a Session across threads: bind each EntityManager to the thread that began the transaction","If a timeout reaper caused it, increase the transaction timeout so rollback happens on the owning thread","Register your own javax.transaction.Synchronization to run cleanup on the completing thread instead of relying on delayed processing","Upgrade to the latest Hibernate 6.x — thread tracking in SynchronizationCallbackCoordinatorTrackingImpl has had multiple fixes"],"exampleFix":"// before\n// tx rolled back by reaper thread; reusing the same EntityManager\nem.find(Order.class, id); // throws \"Transaction was rolled back in a different thread\"\n\n// after\ntry {\n    em.find(Order.class, id);\n}\ncatch (HibernateException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"different thread\")) {\n        quietlyClose(em);\n        em = emf.createEntityManager(); // fresh persistence context\n    }\n    else throw e;\n}","handlingStrategy":"try-catch","validationCode":"// before any session use after an async/timeout-prone transaction\nif (!session.isOpen()\n        || session.getTransaction().getStatus() != TransactionStatus.ACTIVE) {\n    throw new IllegalStateException(\"session/transaction not usable; open a new one\");\n}","typeGuard":null,"tryCatchPattern":"try {\n    session.flush(); // or any operation\n}\ncatch (HibernateException e) {\n    if (e.getMessage() != null && e.getMessage().contains(\"rolled back in a different thread\")) {\n        closeQuietly(session);\n        session = sessionFactory.openSession(); // discard poisoned context\n    }\n    else {\n        throw e;\n    }\n}","preventionTips":["Never share an EntityManager/Session between threads; keep it bound to the thread that started the transaction","Set transaction timeouts generously enough that the transaction-reaper thread never aborts transactions for you","Register cleanup as a javax.transaction.Synchronization so it runs on the completing thread","After any caught timeout or async rollback, discard the session instead of reusing it"],"tags":["jta","transactions","concurrency","session-management"],"backgroundTag":"transaction-completed-in-other-thread","analyzedSha":"fad1729dce015f908198d57a8d80274a30f905a5","analyzedAt":"2026-08-22T04:13:57.527Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}