laurent22/joplin · critical · JoplinError
failSafe
failSafe
Error message
Fail-safe: Sync was interrupted because %d%% of the data (%d items) is about to be deleted. To override this behaviour disable the fail-safe in the sync settings.
What it means
The sync fail-safe: during delta processing, if the share of items that would be deleted is >= 90%, Joplin throws a JoplinError with code 'failSafe' instead of wiping local data. It assumes such massive deletion signals a config error (moved sync dir, disconnected drive returning an empty listing) rather than genuine user intent. Guarded by options.wipeOutFailSafe so it only fires when the loop intends to wipe.
Solutions
- Verify the sync target URL/path is correct and still contains the data (browse it via the provider's UI).
- Re-link the correct account / re-point to the original sync folder.
- If the mass deletion is genuinely intended, disable the fail-safe in sync settings (UI: Synchronisation -> Advanced -> disable fail-safe), then re-sync.
- Check the network drive is mounted / Nextcloud folder wasn't moved before retrying.
Example fix
// before - mass deletion blocked by fail-safe
await synchronizer.sync();
// after - confirm intent, then disable fail-safe only if deletion is real
if (userConfirmedWipe) {
await Setting.setValue('sync.failSafe', false); // expose via sync settings UI
await synchronizer.sync();
await Setting.setValue('sync.failSafe', true);
} Defensive patterns
Strategy: validation
Validate before calling
// Sanity-check the remote listing isn't empty before allowing a wipe.
if (options.wipeOutFailSafe && remoteItemCount < localItemCount * 0.1) {
throw new Error('Refusing to sync: remote listing near-empty. Verify sync target path/account.');
} Type guard
function isFailSafeAbort(e: any): e is { code: 'failSafe' } {
return e && e.code === 'failSafe';
} Try / catch
try {
await synchronizer.sync();
} catch (e) {
if (isFailSafeAbort(e)) {
// Do NOT auto-disable. Prompt the user to verify the sync target first.
throw new Error('Sync aborted by fail-safe. Verify the sync target is correct and still has your data before disabling the fail-safe.');
}
throw e;
} Prevention
- Before disabling the fail-safe, verify the sync target path/account is correct and still contains your data.
- Don't re-point sync to an empty folder; create the folder on the original path instead.
- Confirm network drives are mounted before syncing.
- Treat a fail-safe abort as a red flag, never auto-bypass it.
When it happens
Trigger: basicDelta() compares local item IDs against the remote listing; when nearly all local items are absent remotely (>=90%) and wipeOutFailSafe is set, it aborts. Caused by an empty remote listing that's actually a config/connection problem, not real deletions.
Common situations: User moved/renamed the Nextcloud/WebDAV sync folder so the listed path is empty; network drive disconnected and returns an empty dir; switched sync target accidentally; remote auth changed and returns an empty account; a server bug returns an empty listing.
Related errors
- Cannot initialise synchroniser.
- Could not get lastModified date for resource
- Delta link missing
- href not in baseUrl nor relativeBaseUrl
- Invalid WebDAV resource format
AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12).
Data as JSON: /api/errors/212f3dde8b36e209.
Report an issue: GitHub.
Appendix: source
Thrown at packages/lib/file-api.ts:647
for (let i = 0; i < itemIds.length; i++) {
const itemId = itemIds[i];
if (ArrayUtils.binarySearch(newContext.statIdsCache, itemId) < 0) {
deletedItems.push({
path: BaseItem.systemPath(itemId),
isDeleted: true,
});
}
}
const percentDeleted = itemIds.length ? deletedItems.length / itemIds.length : 0;
// If more than 90% of the notes are going to be deleted, it's most likely a
// configuration error or bug. For example, if the user moves their Nextcloud
// directory, or if a network drive gets disconnected and returns an empty dir
// instead of an error. In that case, we don't wipe out the user data, unless
// they have switched off the fail-safe.
if (options.wipeOutFailSafe && percentDeleted >= 0.90) throw new JoplinError(sprintf('Fail-safe: Sync was interrupted because %d%% of the data (%d items) is about to be deleted. To override this behaviour disable the fail-safe in the sync settings.', Math.round(percentDeleted * 100), deletedItems.length), 'failSafe');
output = output.concat(deletedItems);
}
newContext.deletedItemsProcessed = true;
const hasMore = output.length >= outputLimit;
if (!hasMore) {
// Clear temporary info from context. It's especially important to remove deletedItemsProcessed
// so that they are processed again on the next sync.
newContext.statsCache = null;
newContext.statIdsCache = null;
delete newContext.deletedItemsProcessed;
}
return {
hasMore: hasMore,View on GitHub (pinned to 2654b33620)