laurent22/joplin · error · JoplinError

fetch failed: ${result.statusText}

Error message

fetch failed: ${result.statusText}

What it means

Thrown by the web-platform shim.fetchBlob when the browser fetch() resolves with result.ok === false (HTTP status >= 400). Unlike the mobile variant, this constructs a JoplinError carrying result.status as the error code, with the message 'fetch failed: <statusText>'. Used when running Joplin's web build (e.g. the browser/Electron-web target).

Source

Thrown at packages/app-mobile/utils/shim-init-react/index.web.ts:60

		return fsDriver_;
	};
	shim.fsDriver = fsDriver;
	shim.crypto = joplinCrypto;

	shim.randomBytes = async (count: number) => {
		const buffer = new Uint8Array(count);
		crypto.getRandomValues(buffer);

		return [...buffer];
	};

	shim.fetchBlob = async function(url, options: FetchBlobOptions) {
		const outputPath = options.path;
		if (!outputPath) throw new Error('fetchBlob: Missing outputPath');

		const result = await fetch(url, { method: options.method, headers: options.headers });
		if (!result.ok) {
			throw new JoplinError(`fetch failed: ${result.statusText}`, result.status);
		}

		const blob = await result.blob();
		await fsDriver().writeFile(outputPath, await blob.arrayBuffer(), 'Buffer');

		return {
			ok: result.ok,
			path: outputPath,
			text: () => {
				return result.statusText;
			},
			json: () => {
				return { message: `${result.status}: ${result.statusText}` };
			},
			status: result.status,
			headers: result.headers,
		};
	};

View on GitHub (pinned to 2654b33620)

Solutions

  1. Inspect the JoplinError.code (it is result.status) to branch on 401 (re-auth), 404 (missing), 429 (back off), 5xx (retry).
  2. Verify the URL is correct and the resource exists server-side.
  3. Check CORS headers on the response if running in a browser — a CORS failure may manifest as a network error or non-ok response.
  4. Retry with backoff for 5xx and 429; do not retry 4xx (except 408/429).

Example fix

// before
await shim.fetchBlob(url, { path, method: 'GET' });
// after — branch on the carried status code
try {
	await shim.fetchBlob(url, { path, method: 'GET' });
} catch (e) {
	if (e instanceof JoplinError && e.code === 401) await refreshToken();
	else if (e instanceof JoplinError && e.code >= 500) await retryWithBackoff();
	else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

try { new URL(url); } catch { throw new Error(`Invalid URL: ${url}`); }
await shim.fetchBlob(url, { path, method: 'GET' });

Type guard

function isJoplinError(e) { return e && typeof e.code === 'number'; }

Try / catch

try {
  await shim.fetchBlob(url, { path, method: 'GET' });
} catch (e) {
  if (e instanceof JoplinError) {
    if (e.code === 401) await refreshToken();
    else if (e.code === 429 || e.code >= 500) await retryWithBackoff();
    else throw e;
  } else throw e;
}

Prevention

When it happens

Trigger: Server returns 4xx/5xx (404 for missing resource, 401/403 auth, 500 server error, 502/503 gateway); CORS preflight rejection surfaces as a non-ok response; the sync endpoint moved and returns 404; rate limiting (429).

Common situations: Web build downloading an attachment the server cannot find; auth cookie/token expired; CORS misconfiguration on the sync target; transient 5xx during server maintenance; proxy returning 502.

Related errors


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