laurent22/joplin · info · JoplinError
changedDuringSync
changedDuringSync
Error message
Processing a path that has already been done: %s. The user is making changes while the sync is in progress
What it means
A JoplinError (code 'changedDuringSync') thrown as the default branch when a path is re-encountered in donePaths but none of the special conditions (future timestamp, force_sync) apply. Per the code comment this is expected on mobile: the user keeps typing while a sync runs, generating new outgoing changes that re-add the path to the work list. The sync should continue looping, not stop.
Source
Thrown at packages/lib/Synchronizer.ts:658
// progress. When force_sync is not true, this is because the user is typing while the sync is running, so we should continue
// looping, as we don't want the sync to stop when there are still un-synced outgoing changes, otherwise this creates a race condition
// on mobile, where additional changes made during upload are not synced and don't trigger another sync, whereas a change made immediately
// after the sync has finished will trigger another sync. Once the user has stopped typing, it can then break out of the loop and continue
// the rest of the process.
// - It can also happen if the item is directly modified in the sync target, and set with an update_time in the future. In that case,
// the local sync_time will be updated to Date.now() but on the next loop it will see that the remote item still has a date ahead
// and will see a conflict. There's currently no automatic fix for this - the remote item on the sync target must be fixed manually
// (by setting an updated_time less than current time).
if (donePaths.indexOf(path) >= 0) {
const syncItem = await BaseItem.syncItem(syncTargetId, local.id, { fields: ['force_sync'] });
if (local.updated_time > time.unixMs() + Day) {
throw new Error(sprintf('Remote item %s has an updated_time in the future', path));
} else if (local.updated_time > time.unixMs()) {
throw new JoplinError(sprintf('Processing a path that has already been done: %s. Remote item has an updated_time in the future', path), 'processingPathTwice');
} else if (syncItem.force_sync) {
throw new JoplinError(sprintf('Processing a path that has already been done: %s. Item was marked for sync using force_sync', path), 'processingPathTwice');
} else {
throw new JoplinError(sprintf('Processing a path that has already been done: %s. The user is making changes while the sync is in progress', path), 'changedDuringSync');
}
}
const remote: RemoteItem = result.neverSyncedItemIds.includes(local.id) ? null : await this.apiCall('stat', path);
let action: SyncAction = null;
let itemIsReadOnly = false;
let reason = '';
let remoteContent = null;
const getConflictType = (conflictedItem: { type_?: number }) => {
if (conflictedItem.type_ === BaseModel.TYPE_NOTE) return SyncAction.NoteConflict;
if (conflictedItem.type_ === BaseModel.TYPE_RESOURCE) return SyncAction.ResourceConflict;
return SyncAction.ItemConflict;
};
if (!remote) {
if (!local.sync_time) {
action = SyncAction.CreateRemote;View on GitHub (pinned to 2654b33620)
Solutions
- This is largely benign — let the sync continue; the next sync cycle will pick up the latest changes.
- If sync loops endlessly, stop editing briefly so the sync can break out of the upload loop and finish.
- On mobile, allow the sync to complete before heavy editing sessions.
Defensive patterns
Strategy: retry
Type guard
function isChangedDuringSync(e: unknown): e is JoplinError {
return e instanceof JoplinError && (e as any).code === 'changedDuringSync';
} Try / catch
try {
await synchronizer.start(options);
} catch (error) {
if (isChangedDuringSync(error)) {
// expected on mobile while typing; the next sync cycle picks up changes
scheduleRetry();
return;
}
throw error;
} Prevention
- Allow the sync loop to continue when users are editing — do not abort on 'changedDuringSync'.
- On mobile, let sync finish before heavy editing if loops become excessive.
- Do not treat 'changedDuringSync' as a data-integrity failure.
When it happens
Trigger: The user edits a note (or any item) while a sync is actively uploading/downloading, so the item's path reappears in the to-process list within the same sync run. Most common on mobile where typing and sync overlap.
Common situations: Active note editing on mobile during background sync; rapid successive edits on desktop while a large sync is running; external editors writing to the Joplin database concurrently.
Related errors
- Item not found: ${itemId}
- No folder with id ${id}
- uploadBlob: ${method} ${url}: ${error.toString()}
- Got metadata for path but could not fetch content: ${path}
- No item with ID ${itemId}
AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12).
Data as JSON: /api/errors/b6c0c356295be79f.
Report an issue: GitHub.