{"id":"d091dec464f613b9","repo":"sequelize/sequelize","slug":"attempting-to-update-a-stale-model-instance-mod","errorCode":null,"errorMessage":"Attempting to update a stale model instance: ${modelName}","messagePattern":"Attempting to update a stale model instance: (.+?)","errorType":"exception","errorClass":"OptimisticLockError","httpStatus":null,"severity":"error","filePath":"packages/core/src/model.js","lineNumber":4098,"sourceCode":"      query = 'update';\n      args = [this, this.constructor.table, values, where, options];\n    }\n\n    if (!this.changed() && !this.isNewRecord) {\n      return this;\n    }\n\n    if (this.isNewRecord) {\n      query = 'insert';\n      args = [this, this.constructor.table, values, options];\n    }\n\n    const [result, rowsUpdated] = await this.constructor.queryInterface[query](...args);\n\n    if (versionAttr) {\n      // Check to see that a row was updated, otherwise it's an optimistic locking error.\n      if (rowsUpdated < 1) {\n        throw new SequelizeErrors.OptimisticLockError({\n          modelName: this.constructor.name,\n          values,\n          where,\n        });\n      } else {\n        result.dataValues[versionAttr] = values[versionColumnName];\n      }\n    }\n\n    // Transfer database generated values (defaults, autoincrement, etc)\n    for (const attribute of modelDefinition.attributes.values()) {\n      if (\n        attribute.columnName &&\n        values[attribute.columnName] !== undefined &&\n        attribute.columnName !== attribute.attributeName\n      ) {\n        values[attribute.attributeName] = values[attribute.columnName];\n        // TODO: if a column uses the same name as an attribute, this will break!","sourceCodeStart":4080,"sourceCodeEnd":4116,"githubUrl":"https://github.com/sequelize/sequelize/blob/7e1deec499d5afbb8d1877c2f4d545cead1214ec/packages/core/src/model.js#L4080-L4116","documentation":"An OptimisticLockError thrown during instance.save() when the model has a version attribute (version: true) and the UPDATE affected zero rows. Sequelize implements optimistic locking by incrementing the version in the WHERE clause; if no row matches, another transaction modified (or deleted) the row since it was read, so this instance is stale. The error includes modelName, the attempted values, and the where clause.","triggerScenarios":"Two concurrent transactions read the same row, both increment version, the second's UPDATE matches nothing and throws. Reading a row, holding it in memory, and saving after another request already updated it. Long-lived forms where the row changed underneath.","commonSituations":"High-concurrency edits on the same record (collaborative editing, counters). Tabbed editing of the same entity. Enabling version:true to protect against lost updates and surfacing conflicts. Test suites that re-save stale fixtures.","solutions":["Catch OptimisticLockError and retry: reload the instance, re-apply the user's changes, and save again.","Surface a 409 Conflict to the client and let them merge/re-submit.","If appropriate, switch the hot path to an atomic increment (Model.increment) that does not rely on the version guard.","Ensure the version column is always selected (not excluded via attributes) so the lock check is accurate."],"exampleFix":"// before\nawait instance.save(); // throws OptimisticLockError on conflict\n\n// after - retry on conflict\nasync function saveWithRetry(instance, retries = 3) {\n  for (let i = 0; i < retries; i++) {\n    try {\n      return await instance.save();\n    } catch (e) {\n      if (e.name !== 'SequelizeOptimisticLockError' || i === retries - 1) throw e;\n      await instance.reload();\n    }\n  }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"function isOptimisticLockError(e: unknown): e is import('@sequelize/core').OptimisticLockError {\n  return e instanceof Error && (e as any).name === 'SequelizeOptimisticLockError';\n}","tryCatchPattern":"async function saveWithRetry(instance, applyChanges, retries = 3) {\n  for (let i = 0; i < retries; i++) {\n    try {\n      return await instance.save();\n    } catch (e) {\n      if (e.name !== 'SequelizeOptimisticLockError' || i === retries - 1) throw e;\n      await instance.reload();\n      if (applyChanges) applyChanges(instance); // re-apply user intent after fresh load\n    }\n  }\n}","preventionTips":["Always SELECT the version column when reading for update (do not exclude it via attributes).","Catch OptimisticLockError and reload+retry or return 409 Conflict.","For hot counters, prefer Model.increment (atomic) over read-modify-write save().","Keep the optimistic-lock retry window small to avoid thundering herds."],"tags":["optimistic-locking","concurrency","save","version","race-condition"],"analyzedSha":"7e1deec499d5afbb8d1877c2f4d545cead1214ec","analyzedAt":"2026-08-03T18:58:44.549Z","schemaVersion":2}