Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
517e197
Report DOM events to reportError directly instead of rethrowing
sebmarkbage Mar 12, 2024
bec21b2
Never rethrow at the root
sebmarkbage Mar 12, 2024
742aba4
Log fatal errors as they happen
sebmarkbage Mar 22, 2024
54471d4
Report root errors to the browser so they show up as "uncaught"
sebmarkbage Mar 12, 2024
bc5374d
Polyfill dispatching error event
sebmarkbage Mar 13, 2024
c6dd65e
Remove rethrowing in the commit phase
sebmarkbage Mar 13, 2024
0d7615d
Rethrow global errors that happened during an internal act
sebmarkbage Mar 22, 2024
ef549d4
Rethrow uncaught errors from act instead of logging them
sebmarkbage Mar 22, 2024
9f8a43a
Aggregate errors in internal act
sebmarkbage Mar 22, 2024
8fe758b
Aggregate errors in act
sebmarkbage Mar 22, 2024
254af8d
Use shared queue and only track errors once for internalAct/waitFor
sebmarkbage Mar 23, 2024
b4de7d2
Test error logging recovery without act
sebmarkbage Mar 24, 2024
06e4464
Fix tests that failed due to internalAct now rethrowing non-render er…
sebmarkbage Mar 22, 2024
785c32a
Fix tests
sebmarkbage Mar 22, 2024
e32089f
Fix tests that rely on flushSync to throw
sebmarkbage Mar 22, 2024
175484e
Use internal act for prod testing
sebmarkbage Mar 25, 2024
7344587
Build lint process for the reportGlobalError polyfill
sebmarkbage Mar 25, 2024
613ae34
Fix test
sebmarkbage Mar 27, 2024
d3f0b57
Fix legacy tests
rickhanlonii Mar 26, 2024
c06e47d
Fix legacy tests in ReactDOM-test.js
rickhanlonii Mar 26, 2024
45fb81e
Add back React.Children.only
rickhanlonii Mar 26, 2024
8e3c0ae
Fix useSyncExternalStoreShared-test.js
rickhanlonii Mar 26, 2024
7a07e98
Fix ReactFresh-test.js
rickhanlonii Mar 26, 2024
0928d91
Update error messages
sebmarkbage Mar 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 61 additions & 10 deletions packages/internal-test-utils/ReactInternalTestUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import simulateBrowserEventDispatch from './simulateBrowserEventDispatch';

export {act} from './internalAct';

import {thrownErrors, actingUpdatesScopeDepth} from './internalAct';

function assertYieldsWereCleared(caller) {
const actualYields = SchedulerMock.unstable_clearLog();
if (actualYields.length !== 0) {
Expand Down Expand Up @@ -110,6 +112,14 @@ ${diff(expectedLog, actualLog)}
throw error;
}

function aggregateErrors(errors: Array<mixed>): mixed {
if (errors.length > 1 && typeof AggregateError === 'function') {
// eslint-disable-next-line no-undef
return new AggregateError(errors);
}
return errors[0];
}

export async function waitForThrow(expectedError: mixed): mixed {
assertYieldsWereCleared(waitForThrow);

Expand All @@ -126,39 +136,80 @@ export async function waitForThrow(expectedError: mixed): mixed {
error.message = 'Expected something to throw, but nothing did.';
throw error;
}

const errorHandlerDOM = function (event: ErrorEvent) {
// Prevent logs from reprinting this error.
event.preventDefault();
thrownErrors.push(event.error);
};
const errorHandlerNode = function (err: mixed) {
thrownErrors.push(err);
};
// We track errors that were logged globally as if they occurred in this scope and then rethrow them.
if (actingUpdatesScopeDepth === 0) {
if (
typeof window === 'object' &&
typeof window.addEventListener === 'function'
) {
// We're in a JS DOM environment.
window.addEventListener('error', errorHandlerDOM);
} else if (typeof process === 'object') {
// Node environment
process.on('uncaughtException', errorHandlerNode);
}
}
try {
SchedulerMock.unstable_flushAllWithoutAsserting();
} catch (x) {
thrownErrors.push(x);
} finally {
if (actingUpdatesScopeDepth === 0) {
if (
typeof window === 'object' &&
typeof window.addEventListener === 'function'
) {
// We're in a JS DOM environment.
window.removeEventListener('error', errorHandlerDOM);
} else if (typeof process === 'object') {
// Node environment
process.off('uncaughtException', errorHandlerNode);
}
}
}
if (thrownErrors.length > 0) {
const thrownError = aggregateErrors(thrownErrors);
thrownErrors.length = 0;

if (expectedError === undefined) {
// If no expected error was provided, then assume the caller is OK with
// any error being thrown. We're returning the error so they can do
// their own checks, if they wish.
return x;
return thrownError;
}
if (equals(x, expectedError)) {
return x;
if (equals(thrownError, expectedError)) {
return thrownError;
}
if (
typeof expectedError === 'string' &&
typeof x === 'object' &&
x !== null &&
typeof x.message === 'string'
typeof thrownError === 'object' &&
thrownError !== null &&
typeof thrownError.message === 'string'
) {
if (x.message.includes(expectedError)) {
return x;
if (thrownError.message.includes(expectedError)) {
return thrownError;
} else {
error.message = `
Expected error was not thrown.

${diff(expectedError, x.message)}
${diff(expectedError, thrownError.message)}
`;
throw error;
}
}
error.message = `
Expected error was not thrown.

${diff(expectedError, x)}
${diff(expectedError, thrownError)}
`;
throw error;
}
Expand Down
51 changes: 50 additions & 1 deletion packages/internal-test-utils/internalAct.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,24 @@ import * as Scheduler from 'scheduler/unstable_mock';

import enqueueTask from './enqueueTask';

let actingUpdatesScopeDepth: number = 0;
export let actingUpdatesScopeDepth: number = 0;

export const thrownErrors: Array<mixed> = [];

async function waitForMicrotasks() {
return new Promise(resolve => {
enqueueTask(() => resolve());
});
}

function aggregateErrors(errors: Array<mixed>): mixed {
if (errors.length > 1 && typeof AggregateError === 'function') {
// eslint-disable-next-line no-undef
return new AggregateError(errors);
}
return errors[0];
}

export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
if (Scheduler.unstable_flushUntilNextPaint === undefined) {
throw Error(
Expand Down Expand Up @@ -63,6 +73,28 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
// public version of `act`, though we maybe should in the future.
await waitForMicrotasks();

const errorHandlerDOM = function (event: ErrorEvent) {
// Prevent logs from reprinting this error.
event.preventDefault();
thrownErrors.push(event.error);
};
const errorHandlerNode = function (err: mixed) {
thrownErrors.push(err);
};
// We track errors that were logged globally as if they occurred in this scope and then rethrow them.
if (actingUpdatesScopeDepth === 1) {
if (
typeof window === 'object' &&
typeof window.addEventListener === 'function'
) {
// We're in a JS DOM environment.
window.addEventListener('error', errorHandlerDOM);
} else if (typeof process === 'object') {
// Node environment
process.on('uncaughtException', errorHandlerNode);
}
}

try {
const result = await scope();

Expand Down Expand Up @@ -106,10 +138,27 @@ export async function act<T>(scope: () => Thenable<T>): Thenable<T> {
Scheduler.unstable_flushUntilNextPaint();
} while (true);

if (thrownErrors.length > 0) {
// Rethrow any errors logged by the global error handling.
const thrownError = aggregateErrors(thrownErrors);
thrownErrors.length = 0;
throw thrownError;
}

return result;
} finally {
const depth = actingUpdatesScopeDepth;
if (depth === 1) {
if (
typeof window === 'object' &&
typeof window.addEventListener === 'function'
) {
// We're in a JS DOM environment.
window.removeEventListener('error', errorHandlerDOM);
} else if (typeof process === 'object') {
// Node environment
process.off('uncaughtException', errorHandlerNode);
}
global.IS_REACT_ACT_ENVIRONMENT = previousIsActEnvironment;
}
actingUpdatesScopeDepth = depth - 1;
Expand Down
19 changes: 3 additions & 16 deletions packages/react-dom-bindings/src/events/DOMPluginEventSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import * as SelectEventPlugin from './plugins/SelectEventPlugin';
import * as SimpleEventPlugin from './plugins/SimpleEventPlugin';
import * as FormActionEventPlugin from './plugins/FormActionEventPlugin';

import reportGlobalError from 'shared/reportGlobalError';

type DispatchListener = {
instance: null | Fiber,
listener: Function,
Expand Down Expand Up @@ -226,9 +228,6 @@ export const nonDelegatedEvents: Set<DOMEventName> = new Set([
...mediaEventTypes,
]);

let hasError: boolean = false;
let caughtError: mixed = null;

function executeDispatch(
event: ReactSyntheticEvent,
listener: Function,
Expand All @@ -238,12 +237,7 @@ function executeDispatch(
try {
listener(event);
} catch (error) {
if (!hasError) {
hasError = true;
caughtError = error;
} else {
// TODO: Make sure this error gets logged somehow.
}
reportGlobalError(error);
}
event.currentTarget = null;
}
Expand Down Expand Up @@ -285,13 +279,6 @@ export function processDispatchQueue(
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase);
// event system doesn't use pooling.
}
// This would be a good time to rethrow if any of the event handlers threw.
if (hasError) {
const error = caughtError;
hasError = false;
caughtError = null;
throw error;
}
}

function dispatchEventsForPlugins(
Expand Down
12 changes: 5 additions & 7 deletions packages/react-dom/src/__tests__/InvalidEventListeners-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,11 @@ describe('InvalidEventListeners', () => {
}
window.addEventListener('error', handleWindowError);
try {
await act(() => {
node.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
}),
);
});
node.dispatchEvent(
new MouseEvent('click', {
bubbles: true,
}),
);
} finally {
window.removeEventListener('error', handleWindowError);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,7 @@ describe('ReactBrowserEventEmitter', () => {
});
window.addEventListener('error', errorHandler);
try {
await act(() => {
CHILD.click();
});
CHILD.click();
expect(idCallOrder.length).toBe(3);
expect(idCallOrder[0]).toBe(CHILD);
expect(idCallOrder[1]).toBe(PARENT);
Expand Down
Loading