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
- 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.
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
- 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.
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
- IllegalStateException while registering synchronization
- RollbackException while registering synchronization
- SystemException while registering synchronization
- Unexpected IllegalStateException while marking transaction…
- IllegalStateException while registering synchronization
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;
}
@OverrideView on GitHub (pinned to d6d39ce1c6)