diff --git a/packages/desktop-client/src/browser-server.js b/packages/desktop-client/src/browser-server.js
index e573a665a2..7b254ff180 100644
--- a/packages/desktop-client/src/browser-server.js
+++ b/packages/desktop-client/src/browser-server.js
@@ -25,14 +25,15 @@ const importScriptsWithRetry = async (script, { maxRetries = 5 } = {}) => {
}
// Attempt to retry after a small delay
- await new Promise(resolve =>
- setTimeout(async () => {
- await importScriptsWithRetry(script, {
+ await new Promise((resolve, reject) => {
+ setTimeout(() => {
+ importScriptsWithRetry(script, {
maxRetries: maxRetries - 1,
- });
- resolve();
- }, 5000),
- );
+ })
+ .then(resolve)
+ .catch(reject);
+ }, 5000);
+ });
}
};
@@ -76,9 +77,11 @@ self.addEventListener('message', async event => {
return;
}
+ // A single failed importScripts bricks the SharedWorker until
+ // it's evicted, so retry in production too.
await importScriptsWithRetry(
`${msg.publicUrl}/kcab/kcab.worker.${hash}.js`,
- { maxRetries: isDev ? 5 : 0 },
+ { maxRetries: isDev ? 5 : 3 },
);
backend.initApp(isDev, self).catch(err => {
diff --git a/packages/desktop-client/src/components/FatalError.test.tsx b/packages/desktop-client/src/components/FatalError.test.tsx
index dabe658cf0..a7f3107b3a 100644
--- a/packages/desktop-client/src/components/FatalError.test.tsx
+++ b/packages/desktop-client/src/components/FatalError.test.tsx
@@ -30,7 +30,7 @@ describe('FatalError', () => {
expect(screen.getByText(/IndexedDB/)).toBeInTheDocument();
});
- it('renders the generic simple message for an app-init-failure without a specific cause', () => {
+ it('renders a backend-worker message for a BackendInitFailure', () => {
const error = {
type: 'app-init-failure',
BackendInitFailure: true,
@@ -38,6 +38,16 @@ describe('FatalError', () => {
render(, { wrapper: TestProviders });
+ expect(
+ screen.getByText(/couldn't load a critical backend worker/i),
+ ).toBeInTheDocument();
+ });
+
+ it('renders the generic simple message for an app-init-failure without a specific cause', () => {
+ const error = { type: 'app-init-failure' };
+
+ render(, { wrapper: TestProviders });
+
expect(
screen.getByText(/problem loading the app in this browser version/i),
).toBeInTheDocument();
diff --git a/packages/desktop-client/src/components/FatalError.tsx b/packages/desktop-client/src/components/FatalError.tsx
index 465d726773..5d741a4752 100644
--- a/packages/desktop-client/src/components/FatalError.tsx
+++ b/packages/desktop-client/src/components/FatalError.tsx
@@ -69,10 +69,17 @@ function RenderSimple({ error }: RenderSimpleProps) {
);
+ } else if ('BackendInitFailure' in error && error.BackendInitFailure) {
+ msg = (
+
+
+ Actual couldn't load a critical backend worker. Reload the page to try
+ again; if the problem persists, do a hard refresh to clear any stale
+ cached assets.
+
+
+ );
} else {
- // This indicates the backend failed to initialize. Show the
- // user something at least so they aren't looking at a blank
- // screen
msg = (
@@ -92,19 +99,6 @@ function RenderSimple({ error }: RenderSimpleProps) {
}}
>
{msg}
-
-
- Please get{' '}
-
- in touch
- {' '}
- for support
-
-
);
}
diff --git a/packages/desktop-client/src/shared-browser-server-core.test.ts b/packages/desktop-client/src/shared-browser-server-core.test.ts
index a0b0e1f42f..aa04a04d37 100644
--- a/packages/desktop-client/src/shared-browser-server-core.test.ts
+++ b/packages/desktop-client/src/shared-browser-server-core.test.ts
@@ -172,6 +172,47 @@ describe('SharedWorker coordinator', () => {
expect.objectContaining({ type: 'app-init-failure', error: 'boom' }),
);
});
+
+ it('clears the cached init failure when the client acknowledges it', () => {
+ const leader = connectTab(coordinator);
+ sendInit(leader);
+ simulateWorkerConnect(leader);
+
+ sendMsg(leader, {
+ type: '__from-worker',
+ msg: { type: 'app-init-failure', error: 'boom' },
+ });
+ expect(coordinator.getState().lastAppInitFailure).toEqual(
+ expect.objectContaining({ type: 'app-init-failure' }),
+ );
+
+ sendMsg(leader, { name: '__app-init-failure-acknowledged' });
+ expect(coordinator.getState().lastAppInitFailure).toBeNull();
+
+ const port2 = connectTab(coordinator);
+ sendInit(port2);
+ expect(port2.postMessage).not.toHaveBeenCalledWith(
+ expect.objectContaining({ type: 'app-init-failure' }),
+ );
+ });
+
+ it('clears the cached init failure when a backend later connects successfully', () => {
+ const failedLeader = connectTab(coordinator);
+ sendInit(failedLeader);
+ sendMsg(failedLeader, {
+ type: '__from-worker',
+ msg: { type: 'app-init-failure', error: 'boom' },
+ });
+ expect(coordinator.getState().lastAppInitFailure).not.toBeNull();
+
+ sendMsg(failedLeader, { type: 'tab-closing' });
+
+ const newLeader = connectTab(coordinator);
+ sendInit(newLeader);
+ simulateWorkerConnect(newLeader);
+
+ expect(coordinator.getState().lastAppInitFailure).toBeNull();
+ });
});
// ── Load budget ─────────────────────────────────────────────────────
diff --git a/packages/desktop-client/src/shared-browser-server-core.ts b/packages/desktop-client/src/shared-browser-server-core.ts
index 3c81c78a89..47707f63ad 100644
--- a/packages/desktop-client/src/shared-browser-server-core.ts
+++ b/packages/desktop-client/src/shared-browser-server-core.ts
@@ -100,6 +100,7 @@ export function createCoordinator({
}
function broadcastConnect(budgetId: string) {
+ lastAppInitFailure = null;
const connectMsg = { type: 'connect' };
broadcastToAllInGroup(budgetId, connectMsg);
for (const port of unassignedPorts) {
@@ -544,10 +545,22 @@ export function createCoordinator({
return;
}
+ // Drop the cache once the client has surfaced the failure, so a
+ // subsequent init can re-attempt. Falls through to the leader so
+ // the Worker still stops its retry interval.
+ if (msg.name === '__app-init-failure-acknowledged') {
+ lastAppInitFailure = null;
+ }
+
// ── Initialization ─────────────────────────────────────────
if (msg.type === 'init') {
cachedInitMsg = msg;
+ // The leader that produced the failure is gone, so the cache
+ // can't recover — drop it and let this tab attempt a fresh init.
+ if (lastAppInitFailure && budgetGroups.size === 0) {
+ lastAppInitFailure = null;
+ }
if (lastAppInitFailure) {
port.postMessage(lastAppInitFailure);
} else {
diff --git a/upcoming-release-notes/7761.md b/upcoming-release-notes/7761.md
new file mode 100644
index 0000000000..f92e205901
--- /dev/null
+++ b/upcoming-release-notes/7761.md
@@ -0,0 +1,6 @@
+---
+category: Enhancements
+authors: [MatissJanis]
+---
+
+Surface the backend init failures with a custom error screen and message.