laurent22/joplin · error · Error

Batch delete failed? ${JSON.stringify(check)}

Error message

Batch delete failed? ${JSON.stringify(check)}

What it means

Thrown by the Dropbox driver while polling files/delete_batch/check. The loop accepts only 'complete' (success) and 'in_progress' (keep waiting); any other '.tag' (notably 'failed') aborts with the raw check response JSON. It surfaces Dropbox-side failures of a bulk delete job that the API reports asynchronously.

Source

Thrown at packages/lib/file-api-driver-dropbox.js:248

	}

	async clearRoot() {
		const entries = await this.list('');
		const batchDelete = [];
		for (let i = 0; i < entries.items.length; i++) {
			batchDelete.push({ path: this.makePath_(entries.items[i].path) });
		}

		const response = await this.api().exec('POST', 'files/delete_batch', { entries: batchDelete });
		const jobId = response.async_job_id;

		while (true) {
			const check = await this.api().exec('POST', 'files/delete_batch/check', { async_job_id: jobId });
			if (check['.tag'] === 'complete') break;

			// It returns "failed" if it didn't work but anyway throw an error if it's anything other than complete or in_progress
			if (check['.tag'] !== 'in_progress') {
				throw new Error(`Batch delete failed? ${JSON.stringify(check)}`);
			}
			await time.sleep(2);
		}
	}
}

module.exports = { FileApiDriverDropbox };

View on GitHub (pinned to 2654b33620)

Solutions

  1. Inspect the JSON in the message — the check response contains a 'failures' array naming which entries errored and why; fix or remove those paths and retry.
  2. Verify the Dropbox app has files.content.delete scope and the account still authorizes the app (re-link the sync target).
  3. Reduce batch size or pre-filter paths that no longer exist before calling delete.
  4. Retry the sync — transient 'failed' tags from rate limiting often clear on the next run.

Example fix

// before
await driver.deleteMany(paths);
// after - pre-filter existing paths and surface partial failures
const existing = [];
for (const p of paths) {
  if (await driver.stat(p)) existing.push(p);
}
// wrap call and, on failure, log check.failures for granular handling
try { await driver.deleteMany(existing); }
catch (e) { if (/Batch delete failed/.test(e.message)) logger.error('Dropbox partial:', e.message); throw e; }
Defensive patterns

Strategy: retry

Validate before calling

// Verify paths exist and the job hasn't already failed before relying on the batch.
const valid = [];
for (const p of paths) { if (await driver.stat(p)) valid.push(p); }
if (!valid.length) return; // nothing to delete

Type guard

function isDropboxBatchFailed(check: any): check is { '.tag': 'failed'; failures?: any[] } {
  return check && check['.tag'] === 'failed';
}

Try / catch

try {
  await driver.deleteMany(paths);
} catch (error) {
  const m = error.message.match(/Batch delete failed\? (.*)/s);
  if (m) {
    const check = JSON.parse(m[1]);
    if (isDropboxBatchFailed(check) && check.failures?.length) {
      // retry only the non-failed entries, or report partial
    }
  }
  throw error;
}

Prevention

When it happens

Trigger: Calling this.delete() or clearRoot() with many paths so the driver dispatches files/delete_batch, then polling the job. Dropbox returns check['.tag'] === 'failed' (or any tag other than complete/in_progress) — e.g. a path inside the batch is missing, permission-denied, rate-limited, or the job hit an internal error.

Common situations: One entry in the batch references a non-existent path; Dropbox API quota/rate limiting; app permissions revoked mid-sync; transient Dropbox backend error; very large batch where a subset of entries fails.

Related errors


AI-assisted analysis of laurent22/joplin@2654b33620 (2026-08-12). Data as JSON: /api/errors/850f88da4f9b7842. Report an issue: GitHub.