Skip to content

Commit 334bcbb

Browse files
committed
Remove recursion from handleErrors
Changed the algorithm of handleErrors a bit to ensure that boundaries are not revisited once they are acknowledged. This passes all the error boundary tests and all the incremental tests; however, it's causing an infinite loop in one of the files when I try to run all the tests. Haven't figured out which one yet. It also means I can't run the record-tests script to see which tests might be failing. I feel pretty good about the overall approach here, though.
1 parent 2cc85e6 commit 334bcbb

File tree

2 files changed

+115
-102
lines changed

2 files changed

+115
-102
lines changed

src/renderers/shared/fiber/ReactFiberErrorBoundary.js

Lines changed: 0 additions & 53 deletions
This file was deleted.

src/renderers/shared/fiber/ReactFiberScheduler.js

Lines changed: 115 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
'use strict';
1414

15-
import type { TrappedError } from 'ReactFiberErrorBoundary';
1615
import type { Fiber } from 'ReactFiber';
1716
import type { FiberRoot } from 'ReactFiberRoot';
1817
import type { HostConfig, Deadline } from 'ReactFiberReconciler';
@@ -24,7 +23,6 @@ var ReactFiberCommitWork = require('ReactFiberCommitWork');
2423
var ReactCurrentOwner = require('ReactCurrentOwner');
2524

2625
var { cloneFiber } = require('ReactFiber');
27-
var { trapError, acknowledgeErrorInBoundary } = require('ReactFiberErrorBoundary');
2826

2927
var {
3028
NoWork,
@@ -51,6 +49,7 @@ var {
5149

5250
var {
5351
HostContainer,
52+
ClassComponent,
5453
} = require('ReactTypeOfWork');
5554

5655
if (__DEV__) {
@@ -89,6 +88,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
8988
let isAnimationCallbackScheduled : boolean = false;
9089
let isDeferredCallbackScheduled : boolean = false;
9190

91+
let nextTrappedErrors : Array<TrappedError> | null = null;
92+
let isHandlingErrors : boolean = false;
93+
9294
function scheduleAnimationCallback(callback) {
9395
if (!isAnimationCallbackScheduled) {
9496
isAnimationCallbackScheduled = true;
@@ -246,10 +248,10 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
246248
}
247249
}
248250

249-
// Now that the tree has been committed, we can handle errors.
250-
if (allTrappedErrors) {
251-
handleErrors(allTrappedErrors);
252-
}
251+
nextTrappedErrors = nextTrappedErrors ?
252+
nextTrappedErrors.push.apply(nextTrappedErrors, allTrappedErrors) :
253+
allTrappedErrors;
254+
253255
performTaskWork();
254256
}
255257

@@ -512,13 +514,13 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
512514
switch (priorityLevel) {
513515
case SynchronousPriority:
514516
performSynchronousWorkUnsafe();
515-
return;
517+
break;
516518
case TaskPriority:
517519
performTaskWorkUnsafe(false);
518-
return;
520+
break;
519521
case AnimationPriority:
520522
performAnimationWorkUnsafe();
521-
return;
523+
break;
522524
case HighPriority:
523525
case LowPriority:
524526
case OffscreenPriority:
@@ -527,9 +529,9 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
527529
} else {
528530
performDeferredWorkUnsafe(deadline);
529531
}
530-
return;
532+
break;
531533
default:
532-
return;
534+
break;
533535
}
534536
} catch (error) {
535537
const failedUnitOfWork = nextUnitOfWork;
@@ -541,79 +543,143 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
541543
throw error;
542544
}
543545
const trappedError = trapError(failedUnitOfWork, error);
544-
handleErrors([trappedError]);
546+
if (nextTrappedErrors) {
547+
nextTrappedErrors.push(trappedError);
548+
} else {
549+
nextTrappedErrors = [trappedError];
550+
}
551+
}
552+
553+
// If there were any errors, handle them now
554+
if (nextTrappedErrors) {
555+
handleErrors();
545556
}
546557
}
547558

548-
function handleErrors(initialTrappedErrors : Array<TrappedError>) : void {
549-
let nextTrappedErrors = initialTrappedErrors;
559+
// Boundaries that have been acknowledged
560+
let knownBoundaries : Set<Fiber | null> = new Set();
561+
562+
function handleErrors() : void {
563+
// Prevent recursion
564+
if (isHandlingErrors) {
565+
return;
566+
}
567+
isHandlingErrors = true;
568+
550569
let firstUncaughtError = null;
551570

571+
// All work created by error boundaries should have Task priority
572+
// so that it finishes before this function exits
552573
const previousPriorityContext = priorityContext;
553574
priorityContext = TaskPriority;
554575

555-
// In each phase, we will attempt to pass errors to boundaries and re-render them.
556-
// If we get more errors, we propagate them to higher boundaries in the next iterations.
557-
558-
while (nextTrappedErrors) {
559-
const trappedErrors = nextTrappedErrors;
560-
nextTrappedErrors = null;
561-
562-
// Pass errors to all affected boundaries.
563-
const affectedBoundaries : Set<Fiber> = new Set();
564-
trappedErrors.forEach(trappedError => {
576+
// Keep looping until there are no more trapped errors
577+
while (true) {
578+
while (nextTrappedErrors && nextTrappedErrors.length) {
579+
const trappedError = nextTrappedErrors.shift();
565580
const boundary = trappedError.boundary;
566581
const error = trappedError.error;
567582
if (!boundary) {
568583
firstUncaughtError = firstUncaughtError || error;
569-
return;
584+
continue;
570585
}
571586
// Don't visit boundaries twice.
572-
if (affectedBoundaries.has(boundary)) {
573-
return;
587+
if (knownBoundaries.has(boundary)) {
588+
continue;
574589
}
575-
// Give error boundary a chance to update its state.
590+
576591
try {
592+
knownBoundaries.add(boundary);
593+
// Give error boundary a chance to update its state.
594+
// Updates will be scheduled with Task priority.
577595
acknowledgeErrorInBoundary(boundary, error);
578-
affectedBoundaries.add(boundary);
579596
} catch (nextError) {
580-
// If it throws, propagate the error.
581-
nextTrappedErrors = nextTrappedErrors || [];
582-
nextTrappedErrors.push(trapError(boundary, nextError));
597+
// If an error is thrown, propagate the error to the next boundary
598+
const te = trapError(boundary, nextError);
599+
if (te.boundary) {
600+
// If a boundary is found, push trapped error onto array
601+
nextTrappedErrors.push(te);
602+
} else {
603+
// Otherwise, we'll need to throw
604+
firstUncaughtError = firstUncaughtError || te.error;
605+
}
583606
}
584-
});
607+
}
585608

586-
// We will process an update caused by each error boundary synchronously.
587-
affectedBoundaries.forEach(boundary => {
588-
scheduleUpdate(boundary);
589-
try {
590-
nextUnitOfWork = findNextUnitOfWork();
591-
performTaskWorkUnsafe(true);
592-
} catch (nextError) {
593-
// If it throws, propagate the error.
594-
nextTrappedErrors = nextTrappedErrors || [];
595-
nextTrappedErrors.push(trapError(boundary, nextError));
609+
610+
// Now that we attempt to flush any work that was scheduled by the boundaries
611+
// If this creates errors, they will be pushed to nextTrappedErrors and the loop will continue
612+
try {
613+
performTaskWorkUnsafe(true);
614+
} catch (error) {
615+
const failedUnitOfWork = nextUnitOfWork;
616+
// Reset because it points to the error boundary:
617+
nextUnitOfWork = null;
618+
if (!failedUnitOfWork) {
619+
// We shouldn't end up here because nextUnitOfWork
620+
// should always be set while work is being performed.
621+
throw error;
596622
}
597-
});
598-
}
623+
const trappedError = trapError(failedUnitOfWork, error);
624+
if (nextTrappedErrors) {
625+
nextTrappedErrors.push(trappedError);
626+
} else {
627+
nextTrappedErrors = [trappedError];
628+
}
629+
}
599630

600-
ReactCurrentOwner.current = null;
631+
if (!nextTrappedErrors || firstUncaughtError) {
632+
break;
633+
}
634+
}
601635

636+
nextTrappedErrors = null;
637+
knownBoundaries = new Set();
602638
priorityContext = previousPriorityContext;
639+
isHandlingErrors = false;
603640

604-
// Surface the first error uncaught by the boundaries to the user.
605641
if (firstUncaughtError) {
606642
// We need to make sure any future root can get scheduled despite these errors.
607643
// Currently after throwing, nothing gets scheduled because these fields are set.
608644
// FIXME: this is likely a wrong fix! It's still better than ignoring updates though.
609645
nextScheduledRoot = null;
610646
lastScheduledRoot = null;
611-
612-
// Throw any unhandled errors.
613647
throw firstUncaughtError;
614648
}
615649
}
616650

651+
type TrappedError = {
652+
boundary: Fiber | null,
653+
error: any,
654+
};
655+
656+
function findClosestErrorBoundary(fiber : Fiber): Fiber | null {
657+
let maybeErrorBoundary = fiber.return;
658+
while (maybeErrorBoundary) {
659+
if (maybeErrorBoundary.tag === ClassComponent) {
660+
const instance = maybeErrorBoundary.stateNode;
661+
if (typeof instance.unstable_handleError === 'function' &&
662+
!knownBoundaries.has(maybeErrorBoundary)) {
663+
return maybeErrorBoundary;
664+
}
665+
}
666+
maybeErrorBoundary = maybeErrorBoundary.return;
667+
}
668+
return null;
669+
}
670+
671+
function trapError(fiber : Fiber, error : any) : TrappedError {
672+
return {
673+
boundary: findClosestErrorBoundary(fiber),
674+
error,
675+
};
676+
}
677+
678+
function acknowledgeErrorInBoundary(boundary : Fiber, error : any) {
679+
const instance = boundary.stateNode;
680+
instance.unstable_handleError(error);
681+
}
682+
617683
function scheduleWork(root : FiberRoot) {
618684
let priorityLevel = priorityContext;
619685

0 commit comments

Comments
 (0)