yiisoft/yii2 · error · yii\db\StaleObjectException

The object being updated is outdated.

Error message

The object being updated is outdated.

What it means

BaseActiveRecord::updateInternal() adds optimistic locking to every UPDATE: the lock column is incremented in the SET clause, and the model's current lock value is added to the WHERE clause. If updateAll() then reports 0 affected rows, the stored version no longer matches the loaded model — someone else saved the record in between — and StaleObjectException is thrown.

Source

Thrown at framework/db/BaseActiveRecord.php:832

            return false;
        }
        $values = $this->getDirtyAttributes($attributes);
        if (empty($values)) {
            $this->afterSave(false, $values);
            return 0;
        }
        $condition = $this->getOldPrimaryKey(true);
        $lock = $this->optimisticLock();
        if ($lock !== null) {
            $values[$lock] = $this->$lock + 1;
            $condition[$lock] = $this->$lock;
        }
        // We do not check the return value of updateAll() because it's possible
        // that the UPDATE statement doesn't change anything and thus returns 0.
        $rows = static::updateAll($values, $condition);

        if ($lock !== null && !$rows) {
            throw new StaleObjectException('The object being updated is outdated.');
        }

        // using null as an array offset is deprecated in PHP `8.5`
        if ($lock !== null && isset($values[$lock])) {
            $this->$lock = $values[$lock];
        }

        $changedAttributes = [];
        foreach ($values as $name => $value) {
            $changedAttributes[$name] = isset($this->_oldAttributes[$name]) ? $this->_oldAttributes[$name] : null;
            $this->_oldAttributes[$name] = $value;
        }
        $this->afterSave(false, $changedAttributes);

        return $rows;
    }

    /**

View on GitHub (pinned to 66f00d18a2)

Solutions

  1. Catch StaleObjectException around save(), call refresh(), re-apply the user's changes onto the fresh record, and retry once
  2. Present a conflict message / 409 response instead of silently losing one side's changes
  3. Ensure every write path increments the lock column (avoid raw updateAll on locked tables)
  4. Drop the optimisticLock() override for tables where last-write-wins is acceptable

Example fix

// before
if (!$model->save()) { /* only validation errors handled */ } // StaleObjectException escapes

// after
use yii\db\StaleObjectException;

try {
    $model->save();
} catch (StaleObjectException $e) {
    $model->refresh();
    $model->setAttributes($submittedAttributes);
    $model->save(); // or surface a conflict to the user
}
Defensive patterns

Strategy: retry

Try / catch

use yii\db\StaleObjectException;

$attempt = 0;
do {
    try {
        $model->save();
        break;
    } catch (StaleObjectException $e) {
        $model->refresh();                      // reload current version
        $model->setAttributes($submittedAttrs); // re-apply user changes
        $attempt++;
    }
} while ($attempt < 2); // one retry, then surface conflict

Prevention

When it happens

Trigger: $model->save() (update path) on a record whose optimisticLock() column changed after the model was loaded; concurrent edits from two tabs/sessions; earlier updateAll() writes that modified the version column without incrementing it, causing permanent drift.

Common situations: Long-lived edit forms in admin panels; race between parallel API PATCH requests; batch jobs touching the same rows as interactive users; version column managed inconsistently across write paths.

Related errors


AI-assisted analysis of yiisoft/yii2@66f00d18a2 (2026-08-17). Data as JSON: /api/errors/20e1899bc80d6fa3. Report an issue: GitHub.