Skip to content

Commit d36e006

Browse files
authored
Fix unhandled promise rejection in setupInitialUrlState (#5864)
dispatch(finalizeProfileView()) returns a Promise that was previously discarded. Errors thrown synchronously inside it (e.g. from selectors like computeReferenceCPUDeltaPerMs) were converted to rejected promises by the async function, then escaped as "Uncaught (in promise)" instead of being routed to the FATAL_ERROR state. Adding .catch() dispatches fatalError() on failure so the error UI is shown correctly. finalizeProfileView is still intentionally not awaited since it handles long-running work like symbolication in the background.
1 parent 6b4745d commit d36e006

File tree

2 files changed

+46
-2
lines changed

2 files changed

+46
-2
lines changed

src/actions/app.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,15 @@ export function setupInitialUrlState(
169169
// load process.
170170
withHistoryReplaceStateSync(() => {
171171
dispatch(updateUrlState(urlState));
172-
dispatch(finalizeProfileView(browserConnection));
172+
// finalizeProfileView is intentionally not awaited: it kicks off
173+
// long-running async work like symbolication that should run in the
174+
// background without blocking this action. The .catch() ensures that any
175+
// synchronous errors thrown before the first await (e.g. from selectors)
176+
// are still routed to the FATAL_ERROR state rather than escaping as
177+
// unhandled promise rejections.
178+
dispatch(finalizeProfileView(browserConnection)).catch((error) => {
179+
dispatch(fatalError(error));
180+
});
173181
dispatch(urlSetupDone());
174182
});
175183
};

src/test/components/UrlManager.test.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { render, act } from 'firefox-profiler/test/fixtures/testing-library';
88
import { getView, getUrlSetupPhase } from '../../selectors/app';
99
import { UrlManager } from '../../components/app/UrlManager';
1010
import { blankStore } from '../fixtures/stores';
11+
import * as receiveProfileModule from 'firefox-profiler/actions/receive-profile';
1112
import {
1213
getDataSource,
1314
getHash,
@@ -61,7 +62,13 @@ describe('UrlManager', function () {
6162
waitUntilState(store, (state) => getUrlSetupPhase(state) === phase)
6263
);
6364

64-
return { dispatch, getState, createUrlManager, waitUntilUrlSetupPhase };
65+
return {
66+
store,
67+
dispatch,
68+
getState,
69+
createUrlManager,
70+
waitUntilUrlSetupPhase,
71+
};
6572
}
6673

6774
beforeEach(function () {
@@ -184,6 +191,35 @@ describe('UrlManager', function () {
184191
expect(window.location.search).toBe(searchString);
185192
});
186193

194+
it('routes errors thrown in finalizeProfileView to the FATAL_ERROR state', async function () {
195+
window.fetchMock.get('*', getSerializableProfile());
196+
197+
const error = new Error('Simulated error in finalizeProfileView');
198+
jest
199+
.spyOn(receiveProfileModule, 'finalizeProfileView')
200+
.mockReturnValue(async () => {
201+
throw error;
202+
});
203+
204+
const urlPath = '/public/FAKE_HASH/calltree/';
205+
const { store, getState, createUrlManager } = setup(
206+
urlPath + '?v=' + CURRENT_URL_VERSION
207+
);
208+
209+
createUrlManager();
210+
211+
// urlSetupDone() fires synchronously before the .catch() microtask runs,
212+
// so we wait for the FATAL_ERROR view phase directly instead of waiting
213+
// for the url setup phase.
214+
await act(() =>
215+
waitUntilState(store, (state) => getView(state).phase === 'FATAL_ERROR')
216+
);
217+
218+
const view: any = getView(getState());
219+
expect(view.phase).toBe('FATAL_ERROR');
220+
expect(view.error).toBe(error);
221+
});
222+
187223
it(`fetches profile and sets the phase to done when everything works`, async function () {
188224
window.fetchMock.get('*', getSerializableProfile());
189225

0 commit comments

Comments
 (0)