{"record":{"id":"492e65b7162e4df3","repo":"overleaf/overleaf","slug":"project-state-changed","errorCode":null,"errorMessage":"project state changed","messagePattern":"project state changed","errorType":"error_code","errorClass":"ProjectStateChangedError","httpStatus":409,"severity":"error","filePath":"services/document-updater/app/js/ProjectManager.js","lineNumber":102,"sourceCode":"}\n\nasync function getProjectDocsAndFlushIfOld(\n  projectId,\n  projectStateHash,\n  excludeVersions\n) {\n  const timer = new Metrics.Timer('projectManager.getProjectDocsAndFlushIfOld')\n\n  const projectStateChanged =\n    await RedisManager.promises.checkOrSetProjectState(\n      projectId,\n      projectStateHash\n    )\n\n  // we can't return docs if project structure has changed\n  if (projectStateChanged) {\n    timer.done()\n    throw new Errors.ProjectStateChangedError('project state changed')\n  }\n\n  // project structure hasn't changed, return doc content from redis\n  const docs = []\n  const docIds = await RedisManager.promises.getDocIdsInProject(projectId)\n  for (const docId of docIds) {\n    const { lines, version } =\n      await DocumentManager.promises.getDocAndFlushIfOldWithLock(\n        projectId,\n        docId\n      )\n    docs.push({ _id: docId, lines, v: version })\n  }\n\n  timer.done()\n  return docs\n}\n","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/services/document-updater/app/js/ProjectManager.js#L84-L120","documentation":"getProjectDocsAndFlushIfOld throws Errors.ProjectStateChangedError when the stored project-state hash no longer matches the current hash, meaning the project structure (docs/files) changed since the docs were cached in Redis. Returning cached docs would risk stale or orphaned content, so the operation aborts and the caller must retry after the project is re-flushed.","triggerScenarios":"Calling getProjectDocsAndFlushIfOld when projectStateHash differs from the hash recorded in Redis for the project (projectStructureStateHash mismatch).","commonSituations":"A collaborator added/renamed/deleted files while another process was flushing docs; web->document-updater race where the project was reloaded between hash check and call; stale Redis state after a failed flush.","solutions":["Retry the operation: the caller (web) should catch ProjectStateChangedError, re-request the project via the project history/flush flow, and call again","Trigger a project flush (flushProjectToMongo / resyncProject) to refresh the state hash in Redis","Check for clients calling getProjectDocsAndFlushIfOld with an outdated projectStateHash; fetch a fresh hash from web before calling","Inspect for concurrent structural edits causing repeated races and serialize structural updates"],"exampleFix":"try {\n  await documentUpdaterHandler.promises.getProjectDocsIfMatch(projectId, projectStateHash)\n} catch (err) {\n  if (err instanceof Errors.ProjectStateChangedError) {\n    await projectHistoryManager.promises.resyncProject(projectId)\n    return retry()\n  }\n  throw err\n}","handlingStrategy":"retry","validationCode":"// fetch a fresh projectStateHash from web immediately before the call\nconst projectStateHash = await getProjectStateHash(projectId)","typeGuard":null,"tryCatchPattern":"try {\n  await getProjectDocsAndFlushIfOld(projectId, projectStateHash)\n} catch (err) {\n  if (err.name === 'ProjectStateChangedError') return retryWithFreshHash()\n  throw err\n}","preventionTips":["Always pass a freshly fetched projectStateHash","Minimize time between structural edits and doc flushing","Serialize project structure changes against flush operations","Add exponential backoff on retry to avoid races"],"tags":["document-updater","state-hash-mismatch","stale-state"],"backgroundTag":"project-state-changed","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}