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
- Catch StaleObjectException around save(), call refresh(), re-apply the user's changes onto the fresh record, and retry once
- Present a conflict message / 409 response instead of silently losing one side's changes
- Ensure every write path increments the lock column (avoid raw updateAll on locked tables)
- 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
- Catch StaleObjectException around every save() on optimistic-locked models
- Refresh and re-apply instead of retrying the stale instance blindly
- Keep write windows short: load, save, done — never hold models across user think-time
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.