{"record":{"id":"c319ece09edf74b7","repo":"laurent22/joplin","slug":"lockerror","errorCode":"lockError","errorMessage":"Sync target is locked - aborting API call","messagePattern":"Sync target is locked - aborting API call","errorType":"exception","errorClass":"JoplinError","httpStatus":null,"severity":"error","filePath":"packages/lib/Synchronizer.ts","lineNumber":380,"sourceCode":"\t}\n\n\tprivate async setPpkIfNotExist(localInfo: SyncInfo, remoteInfo: SyncInfo) {\n\t\tif (localInfo.ppk || remoteInfo.ppk) return localInfo;\n\n\t\tconst password = getMasterPassword(false);\n\t\tif (!password) return localInfo;\n\n\t\ttry {\n\t\t\tlocalInfo.ppk = await generateKeyPair(this.encryptionService(), password);\n\t\t} catch (error) {\n\t\t\t// TODO: Remove after RSA encryption is supported on all platforms.\n\t\t\tlogger.error('Failed to generate RSA key pair', error);\n\t\t}\n\t\treturn localInfo;\n\t}\n\n\tprivate async apiCall(fnName: string, ...args: unknown[]) {\n\t\tif (this.syncTargetIsLocked_) throw new JoplinError('Sync target is locked - aborting API call', 'lockError');\n\n\t\ttry {\n\t\t\t// eslint-disable-next-line @typescript-eslint/no-explicit-any -- FileApi exposes many methods with heterogeneous shapes (get/put/list/delete/multiPut/...); dispatching by name keeps the call generic across drivers\n\t\t\tconst output = await (this.api() as any)[fnName](...args);\n\t\t\treturn output;\n\t\t} catch (error) {\n\t\t\tconst lockStatus = await this.lockErrorStatus_();\n\t\t\t// When there's an error due to a lock, we re-wrap the error and change the error code so that error handling\n\t\t\t// does not do special processing on the original error. For example, if a resource could not be downloaded,\n\t\t\t// don't mark it as a \"cannotSyncItem\" since we don't know that.\n\t\t\tif (lockStatus) {\n\t\t\t\tthrow new JoplinError(`Sync target lock error: ${lockStatus}. Original error was: ${error.message}`, 'lockError');\n\t\t\t} else {\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t}\n","sourceCodeStart":362,"sourceCodeEnd":398,"githubUrl":"https://github.com/laurent22/joplin/blob/2654b33620775080d1d59c552259d41e33dad3d2/packages/lib/Synchronizer.ts#L362-L398","documentation":"Thrown at the very top of Synchronizer.apiCall() when the internal flag syncTargetIsLocked_ is already true. That flag is latched on at Synchronizer.ts:582 inside the auto-lock-refresh failure callback — meaning Joplin's sync lock could no longer be refreshed (another client took an exclusive lock, the lock TTL expired, or the network dropped mid-sync). Once latched, every subsequent apiCall short-circuits with this error so the sync aborts cleanly instead of writing to a target it no longer owns.","triggerScenarios":"Calling any Synchronizer method that delegates to apiCall() (stat/get/put/list/delete/multiPut) AFTER the lock-refresh callback has fired. Concretely: a second device starts a full sync and acquires an exclusive lock, the auto-refresh network request fails, or the sync lock TTL elapses while a long upload is still running. The next apiCall in the same sync run hits the guard.","commonSituations":"Two clients syncing against the same target simultaneously; mobile sync interrupted by connectivity loss so the lock can't be refreshed; switching between devices rapidly; long sync runs on slow links where the lock TTL (lockTtl) is shorter than the upload.","solutions":["Ensure no other Joplin client is running a full sync against the same target at the same time; let the other client finish or release its exclusive lock.","Check network connectivity — the lock-refresh request must succeed periodically within lockTtl; fix the connection and trigger a new sync.","If the lock is stuck (a client crashed holding it), wait for the TTL to expire, then retry the sync from a single client.","Review sync target configuration for the correct server/credentials so the lock endpoint is reachable."],"exampleFix":"// No code fix — operational resolution.\n// Ensure only one client syncs at a time and that connectivity is stable\n// for the duration of the sync so lock refresh succeeds.","handlingStrategy":"retry","validationCode":"// Before triggering sync, confirm no other client is actively syncing\n// and that the lock can be acquired.\nconst locks = await synchronizer.lockHandler().locks();\nconst exclusive = await hasActiveLock(\n  locks, await synchronizer.lockHandler().currentDate(),\n  synchronizer.lockHandler().lockTtl, LockType.Exclusive,\n);\nif (exclusive) {\n  // wait for the other client to finish before syncing\n  return;\n}","typeGuard":"// JoplinError narrowing for lock errors\nimport JoplinError from './JoplinError';\nfunction isLockError(e: unknown): e is JoplinError {\n  return e instanceof JoplinError && (e as any).code === 'lockError';\n}","tryCatchPattern":"try {\n  await synchronizer.apiCall('stat', path);\n} catch (error) {\n  if (isLockError(error)) {\n    // stop the current sync and schedule a retry after backoff\n    await synchronizer.cancel();\n    scheduleRetry();\n    return;\n  }\n  throw error;\n}","preventionTips":["Run only one full sync at a time across all clients sharing a target.","Ensure network connectivity is stable for the whole sync so lock refresh succeeds.","Surface lock errors to the user with a 'retry' affordance rather than treating them as fatal."],"tags":["sync","locking","concurrency","network"],"backgroundTag":null,"analyzedSha":"2654b33620775080d1d59c552259d41e33dad3d2","analyzedAt":"2026-08-12T14:26:46.263Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}