overleaf/overleaf · error · VersionOutOfBoundsError
Non-persisted changes can't be applied to base version
Error message
Non-persisted changes can't be applied to base version
What it means
getNonPersistedChanges asks the Redis script for changes after a given baseVersion. Status 'out_of_bounds' means the requested baseVersion is no longer within the non-persisted window — the changes starting at that version were already persisted into postgres (or compacted away) — so a VersionOutOfBoundsError is thrown.
Source
Thrown at services/history-v1/storage/lib/chunk_store/redis.js:487
)
} catch (err) {
metrics.inc('chunk_store.redis.get_non_persisted_changes', 1, {
status: 'error',
})
throw err
}
const status = result[0]
metrics.inc('chunk_store.redis.get_non_persisted_changes', 1, {
status,
})
if (status === 'ok') {
return result[1].map(json => Change.fromRaw(JSON.parse(json)))
} else if (status === 'not_found') {
return []
} else if (status === 'out_of_bounds') {
throw new VersionOutOfBoundsError(
"Non-persisted changes can't be applied to base version",
{ projectId, baseVersion }
)
} else {
throw new OError('unknown status for get_non_persisted_changes', {
projectId,
baseVersion,
status,
})
}
}
rclient.defineCommand('set_persisted_version', {
numberOfKeys: 4,
lua: `
local headVersionKey = KEYS[1]
local persistedVersionKey = KEYS[2]
local persistTimeKey = KEYS[3]View on GitHub (pinned to 28ad3b03b7)
Solutions
- Catch VersionOutOfBoundsError and fall back to loading the persisted chunk history from postgres for versions at/below the persisted boundary.
- Clamp baseVersion to the store's current non-persisted start version before calling.
- Subscribe to/track the persisted version and only call this API for versions > persistedVersion.
Example fix
// before
const changes = await chunkStore.getNonPersistedChanges(projectId, baseVersion)
// after
let changes
try {
changes = await chunkStore.getNonPersistedChanges(projectId, baseVersion)
} catch (err) {
if (err instanceof VersionOutOfBoundsError) {
changes = await loadPersistedChangesFromPostgres(projectId, baseVersion)
} else {
throw err
}
} Defensive patterns
Strategy: fallback
Validate before calling
const persisted = await chunkStore.getProjectPersistedVersion(projectId); if (baseVersion <= persisted) { usePersistedPath() } Type guard
function isVersionOutOfBounds(err) { return err instanceof VersionOutOfBoundsError } Try / catch
try { return await chunkStore.getNonPersistedChanges(projectId, baseVersion) } catch (err) { if (isVersionOutOfBounds(err)) { return await loadFromPostgres(projectId, baseVersion) } throw err } Prevention
- Track the persisted version and never request non-persisted changes below it.
- Prefer range queries over absolute base versions when walking history.
- Remember persistence can happen concurrently; always code the postgres fallback path.
When it happens
Trigger: Calling getNonPersistedChanges(projectId, baseVersion) where baseVersion is below the oldest retained non-persisted change, typically because a persist job already flushed those changes to postgres.
Common situations: A reader computing diff history against a base version that has just been persisted; background persistence racing an interactive read; asking for version 0 on a project whose history start has advanced.
Related errors
- Persisted version cannot be higher than head version
- base version mismatch
- unable to close chunk: not found
- unable to close chunk: already closed
- Cannot queue empty changes array
AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03).
Data as JSON: /api/errors/75e3d7990481a4ef.
Report an issue: GitHub.