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

Unexpected IllegalStateException while marking transaction…

Error message

Unexpected IllegalStateException while marking transaction rollback only

What it means

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.

Solutions

  1. 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.
  2. Verify the transaction manager setup: the configured JtaTransactionManager/TransactionManager must manage transactions on the same thread that runs the Flowable command.
  3. If using Spring, ensure JtaTransactionContext is used consistently with JTA (SpringJtaTransactionManager / jta mode), not mixed with DataSourceTransactionManager semantics.
  4. Check for long-running commands exceeding the app server's transaction timeout; increase the timeout so the transaction is not reaped mid-command.
  5. Catch FlowableException in the command layer and retry the whole unit of work in a fresh transaction.

Example fix

// before: sharing one thread's transaction with async work
executor.submit(() -> processEngine.getRuntimeService().startProcessInstanceByKey("p"));
// after: run the command inside the JTA transaction (same thread)
transactionTemplate.execute(status ->
    processEngine.getRuntimeService().startProcessInstanceByKey("p"));
Defensive patterns

Strategy: try-catch

Validate before calling

Transaction tx = tm.getTransaction();
if (tx != null) {
  int st = tx.getStatus();
  if (st == Status.STATUS_ACTIVE || st == Status.STATUS_MARKED_ROLLBACK) {
    // safe to proceed
  }
}

Type guard

boolean isActiveJtaTransaction(TransactionManager tm) {
  try { return tm.getTransaction() != null && tm.getTransaction().getStatus() == Status.STATUS_ACTIVE; }
  catch (SystemException e) { return false; }
}

Try / catch

try {
  runtimeService.startProcessInstanceByKey("p");
} catch (FlowableException e) {
  if (e.getCause() instanceof IllegalStateException) {
    // transaction lost/aborted: retry in a fresh transaction
    transactionTemplate.execute(s -> runtimeService.startProcessInstanceByKey("p"));
  } else throw e;
}

Prevention

When it happens

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

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

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/2c2baac33ea4cd97. Report an issue: GitHub.

Appendix: source

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

        this.transactionManager = transactionManager;
    }

    @Override
    public void commit() {
        // managed transaction, ignore
    }

    @Override
    public void rollback() {
        // managed transaction, mark rollback-only if not done so already.
        try {
            Transaction transaction = getTransaction();
            int status = transaction.getStatus();
            if (status != Status.STATUS_NO_TRANSACTION && status != Status.STATUS_ROLLEDBACK) {
                transaction.setRollbackOnly();
            }
        } catch (IllegalStateException e) {
            throw new FlowableException("Unexpected IllegalStateException while marking transaction rollback only", e);
        } catch (SystemException e) {
            throw new FlowableException("SystemException while marking transaction rollback only", e);
        }
    }

    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();

View on GitHub (pinned to d6d39ce1c6)