{"record":{"id":"b6c0c356295be79f","repo":"laurent22/joplin","slug":"changedduringsync","errorCode":"changedDuringSync","errorMessage":"Processing a path that has already been done: %s. The user is making changes while the sync is in progress","messagePattern":"Processing a path that has already been done: (.+?)\\. The user is making changes while the sync is in progress","errorType":"exception","errorClass":"JoplinError","httpStatus":null,"severity":"info","filePath":"packages/lib/Synchronizer.ts","lineNumber":658,"sourceCode":"\t\t\t\t\t\t//   progress. When force_sync is not true, this is because the user is typing while the sync is running, so we should continue\n\t\t\t\t\t\t//   looping, as we don't want the sync to stop when there are still un-synced outgoing changes, otherwise this creates a race condition\n\t\t\t\t\t\t//   on mobile, where additional changes made during upload are not synced and don't trigger another sync, whereas a change made immediately\n\t\t\t\t\t\t//   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\n\t\t\t\t\t\t//   the rest of the process.\n\t\t\t\t\t\t// - 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,\n\t\t\t\t\t\t//   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\n\t\t\t\t\t\t//   and will see a conflict. There's currently no automatic fix for this - the remote item on the sync target must be fixed manually\n\t\t\t\t\t\t//   (by setting an updated_time less than current time).\n\t\t\t\t\t\tif (donePaths.indexOf(path) >= 0) {\n\t\t\t\t\t\t\tconst syncItem = await BaseItem.syncItem(syncTargetId, local.id, { fields: ['force_sync'] });\n\t\t\t\t\t\t\tif (local.updated_time > time.unixMs() + Day) {\n\t\t\t\t\t\t\t\tthrow new Error(sprintf('Remote item %s has an updated_time in the future', path));\n\t\t\t\t\t\t\t} else if (local.updated_time > time.unixMs()) {\n\t\t\t\t\t\t\t\tthrow new JoplinError(sprintf('Processing a path that has already been done: %s. Remote item has an updated_time in the future', path), 'processingPathTwice');\n\t\t\t\t\t\t\t} else if (syncItem.force_sync) {\n\t\t\t\t\t\t\t\tthrow new JoplinError(sprintf('Processing a path that has already been done: %s. Item was marked for sync using force_sync', path), 'processingPathTwice');\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tthrow 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');\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst remote: RemoteItem = result.neverSyncedItemIds.includes(local.id) ? null : await this.apiCall('stat', path);\n\t\t\t\t\t\tlet action: SyncAction = null;\n\t\t\t\t\t\tlet itemIsReadOnly = false;\n\t\t\t\t\t\tlet reason = '';\n\t\t\t\t\t\tlet remoteContent = null;\n\n\t\t\t\t\t\tconst getConflictType = (conflictedItem: { type_?: number }) => {\n\t\t\t\t\t\t\tif (conflictedItem.type_ === BaseModel.TYPE_NOTE) return SyncAction.NoteConflict;\n\t\t\t\t\t\t\tif (conflictedItem.type_ === BaseModel.TYPE_RESOURCE) return SyncAction.ResourceConflict;\n\t\t\t\t\t\t\treturn SyncAction.ItemConflict;\n\t\t\t\t\t\t};\n\n\t\t\t\t\t\tif (!remote) {\n\t\t\t\t\t\t\tif (!local.sync_time) {\n\t\t\t\t\t\t\t\taction = SyncAction.CreateRemote;","sourceCodeStart":640,"sourceCodeEnd":676,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/lib/Synchronizer.ts#L640-L676","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"retry","validationCode":null,"typeGuard":"function isChangedDuringSync(e: unknown): e is JoplinError {\n  return e instanceof JoplinError && (e as any).code === 'changedDuringSync';\n}","tryCatchPattern":"try {\n  await synchronizer.start(options);\n} catch (error) {\n  if (isChangedDuringSync(error)) {\n    // expected on mobile while typing; the next sync cycle picks up changes\n    scheduleRetry();\n    return;\n  }\n  throw error;\n}","preventionTips":["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."],"tags":["sync","race-condition","mobile","concurrent-editing"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}