doctrine/orm · critical · OptimisticLockException

Commit failed

Error message

Commit failed

What it means

UnitOfWork::commit() runs the flush inside a DB transaction; if the final COMMIT fails - the driver throws a DBAL exception (deadlock, lock wait timeout, deferred constraint violation, dropped connection) or commit() returns false - Doctrine marks the commit failed, closes the EntityManager, attempts a rollback in the finally block, and rethrows as OptimisticLockException('Commit failed') with the original driver error attached as previous. The OptimisticLockException wrapper is a legacy choice: the failure is normally a database-level commit problem, not an ORM version conflict.

Source

Thrown at src/UnitOfWork.php:446

            }

            // Entity deletions come last. Their order only needs to take care of other deletions
            // (first delete entities depending upon others, before deleting depended-upon entities).
            if ($this->entityDeletions) {
                $this->executeDeletions();
            }

            $commitFailed = false;
            try {
                if ($conn->commit() === false) {
                    $commitFailed = true;
                }
            } catch (DBAL\Exception $e) {
                $commitFailed = true;
            }

            if ($commitFailed) {
                throw new OptimisticLockException('Commit failed', null, $e ?? null);
            }

            $successful = true;
        } finally {
            if (! $successful) {
                $this->em->close();

                if ($conn->isTransactionActive()) {
                    $conn->rollBack();
                }

                $this->afterTransactionRolledBack();
            }
        }

        $this->afterTransactionComplete();

        // Unset removed entities from collections, and take new snapshots from

View on GitHub (pinned to d9b9ff7301)

Solutions

  1. Inspect the previous exception ($e->getPrevious()) to get the real driver error code before deciding what to do.
  2. For deadlock/lock-wait causes (MySQL 1213/1205, PostgreSQL 40P01) retry the whole unit of work with a fresh EntityManager and backoff - data must be re-read, so retry at the use-case level.
  3. Shorten transactions: flush in smaller batches, move slow/non-DB work outside the transaction.
  4. For PostgreSQL deferred-constraint failures the data itself is wrong - fix the violating rows; retrying will fail identically.

Example fix

// before
$em->flush(); // OptimisticLockException: Commit failed, EntityManager is closed afterwards

// after
retry:
try {
    $em->flush();
} catch (OptimisticLockException $e) {
    $code = $e->getPrevious()?->getCode();
    if (in_array($code, ['1213', '1205', '40P01'], true) && $attempts++ < 3) {
        usleep(50000 * $attempts); // backoff
        $em = $this->managerRegistry->resetManager(); // fresh EntityManager, reload entities
        goto retry;
    }
    throw $e;
}
Defensive patterns

Strategy: retry

Try / catch

// Commit failures are usually transient deadlocks: retry with a fresh EM at the use-case level
$maxAttempts = 3;
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
    try {
        $this->doUnitOfWork($input); // (re)load entities, mutate, flush
        return;
    } catch (OptimisticLockException $e) {
        $driverCode = (string) $e->getPrevious()?->getCode();
        $transient  = in_array($driverCode, ['1213', '1205', '40001', '40P01'], true);
        if (! $transient || $attempt === $maxAttempts) {
            throw $e;
        }
        $this->managerRegistry->resetManager(); // closed EM must be replaced
        usleep(50000 * (2 ** $attempt));
    }
}

Prevention

When it happens

Trigger: $em->flush() whose COMMIT statement fails: MySQL/InnoDB deadlock (1213) or innodb_lock_wait_timeout (1205) hitting at commit; PostgreSQL deferred foreign-key/unique constraints failing at COMMIT; the server dropping the connection mid-transaction (wait_timeout, network blip); or a driver/setup where commit() returns false without throwing.

Common situations: Concurrent workers updating overlapping rows under load; long-running transactions; PostgreSQL schemas using DEFERRABLE INITIALLY DEFERRED constraints; batch imports racing each other; cloud databases killing idle connections.

Related errors


AI-assisted analysis of doctrine/orm@d9b9ff7301 (2026-08-21). Data as JSON: /api/errors/ad3ee2dbbe50435f. Report an issue: GitHub.