flowable/flowable-engine · error · org.flowable.common.engine.api.FlowableException

RollbackException while registering synchronization

Error message

RollbackException while registering synchronization 

What it means

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.

Solutions

  1. Find and fix the earlier failure that marked the transaction rollback-only (check logs for the original exception).
  2. Check the transaction's status before registering listeners and skip/defer the listener when the TX is doomed.
  3. Increase the transaction timeout if long-running commands cause TM rollback before listener registration.
  4. Don't swallow exceptions in inner operations — let the command fail early so listeners aren't registered on a rolling-back TX.
  5. Catch this FlowableException at the boundary and roll back the unit of work, retrying in a fresh transaction.

Example fix

// before: ignoring inner exception lets listener registration fail later on a rollback-only TX
try { dataService.update(entity); } catch (Exception e) { log.warn(e); }
commandContext.getTransactionContext().addTransactionListener(COMMITTED, listener);
// after: propagate the failure so the command aborts before listener registration
dataService.update(entity);
commandContext.getTransactionContext().addTransactionListener(COMMITTED, listener);
Defensive patterns

Strategy: try-catch

Validate before calling

int st = tx.getStatus();
if (st == Status.STATUS_MARKED_ROLLBACK || st == Status.STATUS_ROLLING_BACK || st == Status.STATUS_ROLLEDBACK) {
  throw new IllegalStateException("TX marked for rollback, listener cannot be registered");
}

Type guard

boolean isRollbackOnly(Transaction tx) {
  try { int st = tx.getStatus();
        return st == Status.STATUS_MARKED_ROLLBACK || st == Status.STATUS_ROLLING_BACK || st == Status.STATUS_ROLLEDBACK; }
  catch (SystemException e) { return true; }
}

Try / catch

try {
  commandContext.getTransactionContext().addTransactionListener(TransactionState.COMMITTED, listener);
} catch (FlowableException e) {
  if (e.getCause() instanceof RollbackException) {
    // TX will roll back: trigger the ROLLED_BACK path instead of COMMITTED
    listener.execute(CommandContextUtil.getCommandContext());
  } else throw e;
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of flowable/flowable-engine@d6d39ce1c6 (2026-09-11). Data as JSON: /api/errors/e079b24123202f9c. Report an issue: GitHub.

Appendix: source

Thrown at modules/flowable-engine-common/src/main/java/org/flowable/common/engine/impl/cfg/jta/JtaTransactionContext.java:80

    protected Transaction getTransaction() {
        try {
            return transactionManager.getTransaction();
        } catch (SystemException e) {
            throw new FlowableException("SystemException while getting transaction ", e);
        }
    }

    @Override
    public void addTransactionListener(TransactionState transactionState, final TransactionListener transactionListener) {

        Transaction transaction = getTransaction();
        CommandContext commandContext = Context.getCommandContext();
        try {
            transaction.registerSynchronization(new TransactionStateSynchronization(transactionState, transactionListener, commandContext));
        } catch (IllegalStateException e) {
            throw new FlowableException("IllegalStateException while registering synchronization ", e);
        } catch (RollbackException e) {
            throw new FlowableException("RollbackException while registering synchronization ", e);
        } catch (SystemException e) {
            throw new FlowableException("SystemException while registering synchronization ", e);
        }
    }

    public static class TransactionStateSynchronization implements Synchronization {

        protected final TransactionListener transactionListener;
        protected final TransactionState transactionState;
        private final CommandContext commandContext;

        public TransactionStateSynchronization(TransactionState transactionState, TransactionListener transactionListener, CommandContext commandContext) {
            this.transactionState = transactionState;
            this.transactionListener = transactionListener;
            this.commandContext = commandContext;
        }

        @Override

View on GitHub (pinned to d6d39ce1c6)