{"record":{"id":"2c2baac33ea4cd97","repo":"flowable/flowable-engine","slug":"unexpected-illegalstateexception-while-marking-tra","errorCode":null,"errorMessage":"Unexpected IllegalStateException while marking transaction rollback only","messagePattern":"Unexpected IllegalStateException while marking transaction rollback only","errorType":"exception","errorClass":"org.flowable.common.engine.api.FlowableException","httpStatus":null,"severity":"error","filePath":"modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/cfg/jta/JtaTransactionContext.java","lineNumber":56,"sourceCode":"        this.transactionManager = transactionManager;\n    }\n\n    @Override\n    public void commit() {\n        // managed transaction, ignore\n    }\n\n    @Override\n    public void rollback() {\n        // managed transaction, mark rollback-only if not done so already.\n        try {\n            Transaction transaction = getTransaction();\n            int status = transaction.getStatus();\n            if (status != Status.STATUS_NO_TRANSACTION && status != Status.STATUS_ROLLEDBACK) {\n                transaction.setRollbackOnly();\n            }\n        } catch (IllegalStateException e) {\n            throw new FlowableException(\"Unexpected IllegalStateException while marking transaction rollback only\", e);\n        } catch (SystemException e) {\n            throw new FlowableException(\"SystemException while marking transaction rollback only\", e);\n        }\n    }\n\n    protected Transaction getTransaction() {\n        try {\n            return transactionManager.getTransaction();\n        } catch (SystemException e) {\n            throw new FlowableException(\"SystemException while getting transaction \", e);\n        }\n    }\n\n    @Override\n    public void addTransactionListener(TransactionState transactionState, final TransactionListener transactionListener) {\n\n        Transaction transaction = getTransaction();\n        CommandContext commandContext = Context.getCommandContext();","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/flowable/flowable-engine/blob/d6d39ce1c69ff244f2d9dc6af756a9b95e865586/modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/cfg/jta/JtaTransactionContext.java#L38-L74","documentation":"Flowable's JTA transaction context wraps calls to Transaction.setRollbackOnly() in rollback(). If the JTA Transaction manager throws an IllegalStateException (e.g. the transaction is in a state where marking rollback-only is not allowed, or no active transaction is associated with the current thread), it is rethrown as this FlowableException. It signals the JTA transaction cannot be marked for rollback as expected.","triggerScenarios":"Calling commandContext rollback when the JTA Transaction.getStatus() returned an active status but setRollbackOnly() then throws IllegalStateException — typically because the transaction was concurrently committed/rolled back or the thread lost its transaction association between the status check and the call.","commonSituations":"Container-managed transactions where another component completed the transaction mid-command; async/timeouts invalidating the transaction; mixing Flowable's JTA context with Spring @Transactional boundaries; application server quirks (e.g. transaction reaped by a reaper thread).","solutions":["Check for concurrent transaction completion: ensure no other code (timeout, reaper, async thread) commits/rolls back the transaction while a Flowable command is in flight.","Verify the transaction manager setup: the configured JtaTransactionManager/TransactionManager must manage transactions on the same thread that runs the Flowable command.","If using Spring, ensure JtaTransactionContext is used consistently with JTA (SpringJtaTransactionManager / jta mode), not mixed with DataSourceTransactionManager semantics.","Check for long-running commands exceeding the app server's transaction timeout; increase the timeout so the transaction is not reaped mid-command.","Catch FlowableException in the command layer and retry the whole unit of work in a fresh transaction."],"exampleFix":"// before: sharing one thread's transaction with async work\nexecutor.submit(() -> processEngine.getRuntimeService().startProcessInstanceByKey(\"p\"));\n// after: run the command inside the JTA transaction (same thread)\ntransactionTemplate.execute(status ->\n    processEngine.getRuntimeService().startProcessInstanceByKey(\"p\"));","handlingStrategy":"try-catch","validationCode":"Transaction tx = tm.getTransaction();\nif (tx != null) {\n  int st = tx.getStatus();\n  if (st == Status.STATUS_ACTIVE || st == Status.STATUS_MARKED_ROLLBACK) {\n    // safe to proceed\n  }\n}","typeGuard":"boolean isActiveJtaTransaction(TransactionManager tm) {\n  try { return tm.getTransaction() != null && tm.getTransaction().getStatus() == Status.STATUS_ACTIVE; }\n  catch (SystemException e) { return false; }\n}","tryCatchPattern":"try {\n  runtimeService.startProcessInstanceByKey(\"p\");\n} catch (FlowableException e) {\n  if (e.getCause() instanceof IllegalStateException) {\n    // transaction lost/aborted: retry in a fresh transaction\n    transactionTemplate.execute(s -> runtimeService.startProcessInstanceByKey(\"p\"));\n  } else throw e;\n}","preventionTips":["Keep Flowable commands on threads owned by the JTA transaction manager.","Avoid transaction timeouts: size the command work to fit within the TX timeout.","Never commit/roll back the surrounding transaction from inside command logic or listeners.","Keep the transaction framework choice consistent (JTA everywhere, or non-JTA everywhere)."],"tags":["jta","transaction","rollback","flowable"],"backgroundTag":"invalid-state-transition","analyzedSha":"d6d39ce1c69ff244f2d9dc6af756a9b95e865586","analyzedAt":"2026-09-11T06:41:19.413Z","contentChangedAt":"2026-09-11T06:41:19.413Z","schemaVersion":2},"datasetVersion":"2026-09-18T11:17:12.947Z"}