yiisoft/yii2 · error · yii\db\StaleObjectException

The object being deleted is outdated.

Error message

The object being deleted is outdated.

What it means

yii\db\ActiveRecord::deleteInternal() implements optimistic locking: when optimisticLock() returns a column name, that column's in-memory value is added to the DELETE's WHERE clause. If deleteAll() then affects 0 rows, the stored lock value no longer matches the loaded model — another request modified or removed the row in between — and StaleObjectException is thrown.

Source

Thrown at framework/db/ActiveRecord.php:789

     * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful.
     * @throws StaleObjectException
     */
    protected function deleteInternal()
    {
        if (!$this->beforeDelete()) {
            return false;
        }

        // we do not check the return value of deleteAll() because it's possible
        // the record is already deleted in the database and thus the method will return 0
        $condition = $this->getOldPrimaryKey(true);
        $lock = $this->optimisticLock();
        if ($lock !== null) {
            $condition[$lock] = $this->$lock;
        }
        $result = static::deleteAll($condition);
        if ($lock !== null && !$result) {
            throw new StaleObjectException('The object being deleted is outdated.');
        }
        $this->setOldAttributes(null);
        $this->afterDelete();

        return $result;
    }

    /**
     * Returns a value indicating whether the given active record is the same as the current one.
     * The comparison is made by comparing the table names and the primary key values of the two active records.
     * If one of the records [[isNewRecord|is new]] they are also considered not equal.
     * @param ActiveRecord $record record to compare to
     * @return bool whether the two active records refer to the same row in the same database table.
     */
    public function equals($record)
    {
        if ($this->isNewRecord || $record->isNewRecord) {
            return false;

View on GitHub (pinned to 66f00d18a2)

Solutions

  1. Wrap delete() in try/catch (StaleObjectException), refresh() the model, then either retry the delete or report the conflict to the user
  2. Verify optimisticLock() returns the intended version column and that every write path increments it
  3. Remove or narrow the optimisticLock() override if the table genuinely does not need versioned deletes
  4. Reload the model immediately before deleting instead of reusing a long-held instance

Example fix

// before
$model->delete(); // may throw StaleObjectException

// after
use yii\db\StaleObjectException;

try {
    $model->delete();
} catch (StaleObjectException $e) {
    $model->refresh();
    // row changed or vanished: decide explicitly
    Yii::$app->session->setFlash('warning', 'Record was modified by another user.');
}
Defensive patterns

Strategy: try-catch

Try / catch

use yii\db\StaleObjectException;

try {
    $model->delete();
} catch (StaleObjectException $e) {
    $model->refresh();
    if ($model->getOldPrimaryKey(false) !== null) {
        // version changed but row still exists: let the user confirm
        Yii::$app->session->setFlash('warning', 'Record changed by another user. Please retry.');
    }
    // row gone: treat as deleted
}

Prevention

When it happens

Trigger: $model->delete() where the record's optimistic-lock version changed after the model was loaded (concurrent save in another tab or request); deleting a row another process already deleted; a prior updateAll() changed the version column without incrementing it.

Common situations: Two users editing/deleting the same record; long-lived edit forms; API clients retrying with stale payloads; version column drift caused by batch updates that bypass optimistic locking.

Related errors


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