hibernate/hibernate-orm · error · RollbackException

Transaction was marked for rollback only

Error message

Transaction was marked for rollback only

What it means

jakarta.persistence.RollbackException (javax.persistence pre-Hibernate 6) from the resource-local coordinator's commitRollbackOnly(): the transaction was marked rollback-only — via setRollbackOnly() or by Hibernate after a failed flush — so commit() rolled back instead, and because JPA transaction compliance is enabled (the default in Hibernate 6) that outcome is reported with this exception. The data is rolled back; the message tells you the transaction was poisoned earlier.

Source

Thrown at hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jdbc/internal/JdbcResourceLocalTransactionCoordinatorImpl.java:306

			catch (RuntimeException e) {
				// commit failed
				try {
					afterCompletionCallback( StatusTranslator.STATUS_FAILED_COMMIT );
				}
				catch (RuntimeException e2) {
					e.addSuppressed( e2 );
				}
				throw e;
			}
			// commit successful
			afterCompletionCallback( Status.STATUS_COMMITTED );
		}

		private void commitRollbackOnly() {
			JDBC_LOGGER.onCommitMarkedRollbackOnlyRollingBack();
			rollback();
			if ( jpaCompliance.isJpaTransactionComplianceEnabled() ) {
				throw new RollbackException( "Transaction was marked for rollback only" );
			}
		}

		@Override
		public void rollback() {
			if ( isActive() ) {
				jdbcResourceTransaction.rollback();
				afterCompletionCallback( Status.STATUS_ROLLEDBACK );
			}
		}

		@Override
		@Nonnull
		public TransactionStatus getStatus() {
			return jdbcResourceTransaction.getStatus();
		}

		@Override

View on GitHub (pinned to fad1729dce)

Solutions

  1. Treat any persistence exception as terminal: roll back and start a new transaction instead of committing
  2. In Spring, let the inner exception propagate, or annotate the inner work REQUIRES_NEW when its failure must not poison the outer transaction
  3. Check tx.getRollbackOnly() before commit and roll back deliberately with a clear application error
  4. If you intentionally called setRollbackOnly(), call rollback() — never commit()

Example fix

// before
try { em.flush(); } catch (PersistenceException e) { log.warn("continuing", e); }
em.getTransaction().commit(); // RollbackException: Transaction was marked for rollback only

// after
try {
  em.flush();
  em.getTransaction().commit();
} catch (PersistenceException e) {
  if (em.getTransaction().isActive()) em.getTransaction().rollback();
  throw e; // never commit a transaction that saw a persistence exception
}
Defensive patterns

Strategy: validation

Validate before calling

// decide deliberately before commit
EntityTransaction tx = em.getTransaction();
if (tx.getRollbackOnly()) {
  tx.rollback();
  throw new IllegalStateException("transaction was marked rollback-only; rolled back cleanly");
}
tx.commit();

Type guard

static boolean isPoisoned(jakarta.persistence.EntityTransaction tx) {
  return tx.getRollbackOnly();
}

Try / catch

try {
  tx.commit();
} catch (jakarta.persistence.RollbackException e) {
  // commit already rolled back: close/discard the EntityManager and surface the ORIGINAL
  // exception that poisoned the transaction (look at your logs), then retry in a new transaction
}

Prevention

When it happens

Trigger: tx.setRollbackOnly() followed by tx.commit(); an exception during flush/SQL that the application caught and swallowed — Hibernate marks rollback-only — and commit() is attempted anyway; with Spring, an inner @Transactional (default REQUIRED) whose exception was caught by an outer caller marks the shared transaction rollback-only, so the outer commit throws this.

Common situations: Catch-and-continue error handling inside a transaction; Spring nested service calls where the caller swallows a RuntimeException from a transactional callee; JPA compliance enabled by default after upgrading to Hibernate 6 making previously silent rollbacks visible.

Related errors


AI-assisted analysis of hibernate/hibernate-orm@fad1729dce (2026-08-22). Data as JSON: /api/errors/fb9ddc0fb4905129. Report an issue: GitHub.