overleaf/overleaf · error · ProjectStateChangedError
project state changed
Error message
project state changed
What it means
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.
Source
Thrown at services/document-updater/app/js/ProjectManager.js:102
}
async function getProjectDocsAndFlushIfOld(
projectId,
projectStateHash,
excludeVersions
) {
const timer = new Metrics.Timer('projectManager.getProjectDocsAndFlushIfOld')
const projectStateChanged =
await RedisManager.promises.checkOrSetProjectState(
projectId,
projectStateHash
)
// we can't return docs if project structure has changed
if (projectStateChanged) {
timer.done()
throw new Errors.ProjectStateChangedError('project state changed')
}
// project structure hasn't changed, return doc content from redis
const docs = []
const docIds = await RedisManager.promises.getDocIdsInProject(projectId)
for (const docId of docIds) {
const { lines, version } =
await DocumentManager.promises.getDocAndFlushIfOldWithLock(
projectId,
docId
)
docs.push({ _id: docId, lines, v: version })
}
timer.done()
return docs
}
View on GitHub (pinned to 28ad3b03b7)
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
Example fix
try {
await documentUpdaterHandler.promises.getProjectDocsIfMatch(projectId, projectStateHash)
} catch (err) {
if (err instanceof Errors.ProjectStateChangedError) {
await projectHistoryManager.promises.resyncProject(projectId)
return retry()
}
throw err
} Defensive patterns
Strategy: retry
Validate before calling
// fetch a fresh projectStateHash from web immediately before the call const projectStateHash = await getProjectStateHash(projectId)
Try / catch
try {
await getProjectDocsAndFlushIfOld(projectId, projectStateHash)
} catch (err) {
if (err.name === 'ProjectStateChangedError') return retryWithFreshHash()
throw err
} Prevention
- 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
When it happens
Trigger: Calling getProjectDocsAndFlushIfOld when projectStateHash differs from the hash recorded in Redis for the project (projectStructureStateHash mismatch).
Common situations: 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.
Related errors
- document not found
- doc would become too large if appending this text
- document not found: ${docId}
- comment not found
- document not found: ${docId}
AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03).
Data as JSON: /api/errors/492e65b7162e4df3.
Report an issue: GitHub.