{"record":{"id":"e079b24123202f9c","repo":"flowable/flowable-engine","slug":"rollbackexception-while-registering-synchronizatio","errorCode":null,"errorMessage":"RollbackException while registering synchronization ","messagePattern":"RollbackException while registering synchronization ","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":80,"sourceCode":"    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();\n        try {\n            transaction.registerSynchronization(new TransactionStateSynchronization(transactionState, transactionListener, commandContext));\n        } catch (IllegalStateException e) {\n            throw new FlowableException(\"IllegalStateException while registering synchronization \", e);\n        } catch (RollbackException e) {\n            throw new FlowableException(\"RollbackException while registering synchronization \", e);\n        } catch (SystemException e) {\n            throw new FlowableException(\"SystemException while registering synchronization \", e);\n        }\n    }\n\n    public static class TransactionStateSynchronization implements Synchronization {\n\n        protected final TransactionListener transactionListener;\n        protected final TransactionState transactionState;\n        private final CommandContext commandContext;\n\n        public TransactionStateSynchronization(TransactionState transactionState, TransactionListener transactionListener, CommandContext commandContext) {\n            this.transactionState = transactionState;\n            this.transactionListener = transactionListener;\n            this.commandContext = commandContext;\n        }\n\n        @Override","sourceCodeStart":62,"sourceCodeEnd":98,"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#L62-L98","documentation":"JtaTransactionContext.addTransactionListener() registers a Synchronization on the JTA Transaction; a RollbackException from registerSynchronization is wrapped as this FlowableException. Per JTA spec, RollbackException means the transaction has been marked or decided for rollback, so the synchronization cannot be registered and the listener will never fire via that transaction.","triggerScenarios":"Registering a transaction listener when the JTA transaction is marked rollback-only or already being rolled back (setRollbackOnly was called earlier, or the TX timed out and the container marked it rollback).","commonSituations":"An earlier command failure marked the TX rollback-only and subsequent listener registration fails; transaction timeout during a long Flowable command; business logic that threw but was swallowed, leaving the TX marked rollback-only.","solutions":["Find and fix the earlier failure that marked the transaction rollback-only (check logs for the original exception).","Check the transaction's status before registering listeners and skip/defer the listener when the TX is doomed.","Increase the transaction timeout if long-running commands cause TM rollback before listener registration.","Don't swallow exceptions in inner operations — let the command fail early so listeners aren't registered on a rolling-back TX.","Catch this FlowableException at the boundary and roll back the unit of work, retrying in a fresh transaction."],"exampleFix":"// before: ignoring inner exception lets listener registration fail later on a rollback-only TX\ntry { dataService.update(entity); } catch (Exception e) { log.warn(e); }\ncommandContext.getTransactionContext().addTransactionListener(COMMITTED, listener);\n// after: propagate the failure so the command aborts before listener registration\ndataService.update(entity);\ncommandContext.getTransactionContext().addTransactionListener(COMMITTED, listener);","handlingStrategy":"try-catch","validationCode":"int st = tx.getStatus();\nif (st == Status.STATUS_MARKED_ROLLBACK || st == Status.STATUS_ROLLING_BACK || st == Status.STATUS_ROLLEDBACK) {\n  throw new IllegalStateException(\"TX marked for rollback, listener cannot be registered\");\n}","typeGuard":"boolean isRollbackOnly(Transaction tx) {\n  try { int st = tx.getStatus();\n        return st == Status.STATUS_MARKED_ROLLBACK || st == Status.STATUS_ROLLING_BACK || st == Status.STATUS_ROLLEDBACK; }\n  catch (SystemException e) { return true; }\n}","tryCatchPattern":"try {\n  commandContext.getTransactionContext().addTransactionListener(TransactionState.COMMITTED, listener);\n} catch (FlowableException e) {\n  if (e.getCause() instanceof RollbackException) {\n    // TX will roll back: trigger the ROLLED_BACK path instead of COMMITTED\n    listener.execute(CommandContextUtil.getCommandContext());\n  } else throw e;\n}","preventionTips":["Fail fast on inner exceptions instead of swallowing them so the TX isn't left rollback-only.","Check transaction status before registering commit-time listeners.","Raise the TX timeout for long-running process commands.","Investigate any earlier setRollbackOnly source (timeouts, constraint violations) in logs."],"tags":["jta","transaction","rollback","synchronization","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"}