Skip to content

Commit 753d387

Browse files
committed
Create virtual Fiber when an error occurs during reconcilation
This lets us rethrow it in the conceptual place of the child.
1 parent c4b433f commit 753d387

File tree

7 files changed

+91
-61
lines changed

7 files changed

+91
-61
lines changed

packages/react-client/src/__tests__/ReactFlight-test.js

Lines changed: 23 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -964,67 +964,47 @@ describe('ReactFlight', () => {
964964
const testCases = (
965965
<>
966966
<ClientErrorBoundary expectedMessage="This is a real Error.">
967-
<div>
968-
<Throw value={new TypeError('This is a real Error.')} />
969-
</div>
967+
<Throw value={new TypeError('This is a real Error.')} />
970968
</ClientErrorBoundary>
971969
<ClientErrorBoundary expectedMessage="This is a string error.">
972-
<div>
973-
<Throw value="This is a string error." />
974-
</div>
970+
<Throw value="This is a string error." />
975971
</ClientErrorBoundary>
976972
<ClientErrorBoundary expectedMessage="{message: ..., extra: ..., nested: ...}">
977-
<div>
978-
<Throw
979-
value={{
980-
message: 'This is a long message',
981-
extra: 'properties',
982-
nested: {more: 'prop'},
983-
}}
984-
/>
985-
</div>
973+
<Throw
974+
value={{
975+
message: 'This is a long message',
976+
extra: 'properties',
977+
nested: {more: 'prop'},
978+
}}
979+
/>
986980
</ClientErrorBoundary>
987981
<ClientErrorBoundary
988982
expectedMessage={'{message: "Short", extra: ..., nested: ...}'}>
989-
<div>
990-
<Throw
991-
value={{
992-
message: 'Short',
993-
extra: 'properties',
994-
nested: {more: 'prop'},
995-
}}
996-
/>
997-
</div>
983+
<Throw
984+
value={{
985+
message: 'Short',
986+
extra: 'properties',
987+
nested: {more: 'prop'},
988+
}}
989+
/>
998990
</ClientErrorBoundary>
999991
<ClientErrorBoundary expectedMessage="Symbol(hello)">
1000-
<div>
1001-
<Throw value={Symbol('hello')} />
1002-
</div>
992+
<Throw value={Symbol('hello')} />
1003993
</ClientErrorBoundary>
1004994
<ClientErrorBoundary expectedMessage="123">
1005-
<div>
1006-
<Throw value={123} />
1007-
</div>
995+
<Throw value={123} />
1008996
</ClientErrorBoundary>
1009997
<ClientErrorBoundary expectedMessage="undefined">
1010-
<div>
1011-
<Throw value={undefined} />
1012-
</div>
998+
<Throw value={undefined} />
1013999
</ClientErrorBoundary>
10141000
<ClientErrorBoundary expectedMessage="<div/>">
1015-
<div>
1016-
<Throw value={<div />} />
1017-
</div>
1001+
<Throw value={<div />} />
10181002
</ClientErrorBoundary>
10191003
<ClientErrorBoundary expectedMessage="function Foo() {}">
1020-
<div>
1021-
<Throw value={function Foo() {}} />
1022-
</div>
1004+
<Throw value={function Foo() {}} />
10231005
</ClientErrorBoundary>
10241006
<ClientErrorBoundary expectedMessage={'["array"]'}>
1025-
<div>
1026-
<Throw value={['array']} />
1027-
</div>
1007+
<Throw value={['array']} />
10281008
</ClientErrorBoundary>
10291009
<ClientErrorBoundary
10301010
expectedMessage={
@@ -1034,9 +1014,7 @@ describe('ReactFlight', () => {
10341014
'- A library pre-bundled an old copy of "react" or "react/jsx-runtime".\n' +
10351015
'- A compiler tries to "inline" JSX instead of using the runtime.'
10361016
}>
1037-
<div>
1038-
<LazyInlined />
1039-
</div>
1017+
<LazyInlined />
10401018
</ClientErrorBoundary>
10411019
</>
10421020
);

packages/react-devtools-shared/src/backend/renderer.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ export function getInternalReactConstants(version: string): {
268268
TracingMarkerComponent: 25, // Experimental - This is technically in 18 but we don't
269269
// want to fork again so we're adding it here instead
270270
YieldComponent: -1, // Removed
271+
Throw: 29,
271272
};
272273
} else if (gte(version, '17.0.0-alpha')) {
273274
ReactTypeOfWork = {
@@ -302,6 +303,7 @@ export function getInternalReactConstants(version: string): {
302303
SuspenseListComponent: 19, // Experimental
303304
TracingMarkerComponent: -1, // Doesn't exist yet
304305
YieldComponent: -1, // Removed
306+
Throw: -1, // Doesn't exist yet
305307
};
306308
} else if (gte(version, '16.6.0-beta.0')) {
307309
ReactTypeOfWork = {
@@ -336,6 +338,7 @@ export function getInternalReactConstants(version: string): {
336338
SuspenseListComponent: 19, // Experimental
337339
TracingMarkerComponent: -1, // Doesn't exist yet
338340
YieldComponent: -1, // Removed
341+
Throw: -1, // Doesn't exist yet
339342
};
340343
} else if (gte(version, '16.4.3-alpha')) {
341344
ReactTypeOfWork = {
@@ -370,6 +373,7 @@ export function getInternalReactConstants(version: string): {
370373
SuspenseListComponent: -1, // Doesn't exist yet
371374
TracingMarkerComponent: -1, // Doesn't exist yet
372375
YieldComponent: -1, // Removed
376+
Throw: -1, // Doesn't exist yet
373377
};
374378
} else {
375379
ReactTypeOfWork = {
@@ -404,6 +408,7 @@ export function getInternalReactConstants(version: string): {
404408
SuspenseListComponent: -1, // Doesn't exist yet
405409
TracingMarkerComponent: -1, // Doesn't exist yet
406410
YieldComponent: 9,
411+
Throw: -1, // Doesn't exist yet
407412
};
408413
}
409414
// **********************************************************
@@ -445,6 +450,7 @@ export function getInternalReactConstants(version: string): {
445450
SuspenseComponent,
446451
SuspenseListComponent,
447452
TracingMarkerComponent,
453+
Throw,
448454
} = ReactTypeOfWork;
449455

450456
function resolveFiberType(type: any): $FlowFixMe {
@@ -551,6 +557,9 @@ export function getInternalReactConstants(version: string): {
551557
return 'Profiler';
552558
case TracingMarkerComponent:
553559
return 'TracingMarker';
560+
case Throw:
561+
// This should really never be visible.
562+
return 'Error';
554563
default:
555564
const typeSymbol = getTypeSymbol(type);
556565

@@ -672,6 +681,7 @@ export function attach(
672681
SuspenseComponent,
673682
SuspenseListComponent,
674683
TracingMarkerComponent,
684+
Throw,
675685
} = ReactTypeOfWork;
676686
const {
677687
ImmediatePriority,
@@ -1036,6 +1046,7 @@ export function attach(
10361046
case HostText:
10371047
case LegacyHiddenComponent:
10381048
case OffscreenComponent:
1049+
case Throw:
10391050
return true;
10401051
case HostRoot:
10411052
// It is never valid to filter the root element.

packages/react-devtools-shared/src/backend/types.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type WorkTagMap = {
7272
SuspenseListComponent: WorkTag,
7373
TracingMarkerComponent: WorkTag,
7474
YieldComponent: WorkTag,
75+
Throw: WorkTag,
7576
};
7677

7778
// TODO: If it's useful for the frontend to know which types of data an Element has

packages/react-reconciler/src/ReactChildFiber.js

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,16 @@ import {
5555
createFiberFromFragment,
5656
createFiberFromText,
5757
createFiberFromPortal,
58+
createFiberFromThrow,
5859
} from './ReactFiber';
5960
import {isCompatibleFamilyForHotReloading} from './ReactFiberHotReloading';
6061
import {getIsHydrating} from './ReactFiberHydrationContext';
6162
import {pushTreeFork} from './ReactFiberTreeContext';
62-
import {createThenableState, trackUsedThenable} from './ReactFiberThenable';
63+
import {
64+
SuspenseException,
65+
createThenableState,
66+
trackUsedThenable,
67+
} from './ReactFiberThenable';
6368
import {readContextDuringReconciliation} from './ReactFiberNewContext';
6469
import {callLazyInitInDEV} from './ReactFiberCallUserSpace';
6570

@@ -1888,20 +1893,36 @@ function createChildReconciler(
18881893
newChild: any,
18891894
lanes: Lanes,
18901895
): Fiber | null {
1891-
// This indirection only exists so we can reset `thenableState` at the end.
1892-
// It should get inlined by Closure.
1893-
thenableIndexCounter = 0;
1894-
const firstChildFiber = reconcileChildFibersImpl(
1895-
returnFiber,
1896-
currentFirstChild,
1897-
newChild,
1898-
lanes,
1899-
null, // debugInfo
1900-
);
1901-
thenableState = null;
1902-
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
1903-
// set at the beginning.
1904-
return firstChildFiber;
1896+
try {
1897+
// This indirection only exists so we can reset `thenableState` at the end.
1898+
// It should get inlined by Closure.
1899+
thenableIndexCounter = 0;
1900+
const firstChildFiber = reconcileChildFibersImpl(
1901+
returnFiber,
1902+
currentFirstChild,
1903+
newChild,
1904+
lanes,
1905+
null, // debugInfo
1906+
);
1907+
thenableState = null;
1908+
// Don't bother to reset `thenableIndexCounter` to 0 because it always gets
1909+
// set at the beginning.
1910+
return firstChildFiber;
1911+
} catch (x) {
1912+
if (x === SuspenseException) {
1913+
// Suspense exceptions need to read the current suspended state before
1914+
// yielding and replay it using the same sequence so this trick doesn't
1915+
// work here.
1916+
throw x;
1917+
}
1918+
// Something errored during reconciliation but it's conceptually a child that
1919+
// errored and not the current component itself so we create a virtual child
1920+
// that throws in its begin phase. That way the current component can handle
1921+
// the error or suspending if needed.
1922+
const throwFiber = createFiberFromThrow(x, returnFiber.mode, lanes);
1923+
throwFiber.return = returnFiber;
1924+
return throwFiber;
1925+
}
19051926
}
19061927

19071928
return reconcileChildFibers;

packages/react-reconciler/src/ReactFiber.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
OffscreenComponent,
6868
LegacyHiddenComponent,
6969
TracingMarkerComponent,
70+
Throw,
7071
} from './ReactWorkTags';
7172
import {OffscreenVisible} from './ReactFiberActivityComponent';
7273
import {getComponentNameFromOwner} from 'react-reconciler/src/getComponentNameFromFiber';
@@ -879,3 +880,13 @@ export function createFiberFromPortal(
879880
};
880881
return fiber;
881882
}
883+
884+
export function createFiberFromThrow(
885+
error: mixed,
886+
mode: TypeOfMode,
887+
lanes: Lanes,
888+
): Fiber {
889+
const fiber = createFiber(Throw, error, null, mode);
890+
fiber.lanes = lanes;
891+
return fiber;
892+
}

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
LegacyHiddenComponent,
7373
CacheComponent,
7474
TracingMarkerComponent,
75+
Throw,
7576
} from './ReactWorkTags';
7677
import {
7778
NoFlags,
@@ -4126,6 +4127,11 @@ function beginWork(
41264127
}
41274128
break;
41284129
}
4130+
case Throw: {
4131+
// This represents a Component that threw in the reconciliation phase.
4132+
// So we'll rethrow here. This might be
4133+
throw workInProgress.pendingProps;
4134+
}
41294135
}
41304136

41314137
throw new Error(

packages/react-reconciler/src/ReactWorkTags.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ export type WorkTag =
3636
| 25
3737
| 26
3838
| 27
39-
| 28;
39+
| 28
40+
| 29;
4041

4142
export const FunctionComponent = 0;
4243
export const ClassComponent = 1;
@@ -65,3 +66,4 @@ export const TracingMarkerComponent = 25;
6566
export const HostHoistable = 26;
6667
export const HostSingleton = 27;
6768
export const IncompleteFunctionComponent = 28;
69+
export const Throw = 29;

0 commit comments

Comments
 (0)