Skip to content

Commit 5f3b376

Browse files
author
Brian Vaughn
authored
Show different error boundary UI for timeouts than normal errors (#22483)
1 parent bdd6d50 commit 5f3b376

File tree

7 files changed

+146
-31
lines changed

7 files changed

+146
-31
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
export default class TimeoutError extends Error {
11+
constructor(message: string) {
12+
super(message);
13+
14+
// Maintains proper stack trace for where our error was thrown (only available on V8)
15+
if (Error.captureStackTrace) {
16+
Error.captureStackTrace(this, TimeoutError);
17+
}
18+
19+
this.name = 'TimeoutError';
20+
}
21+
}

packages/react-devtools-shared/src/backendAPI.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import {hydrate, fillInPath} from 'react-devtools-shared/src/hydration';
1111
import {separateDisplayNameAndHOCs} from 'react-devtools-shared/src/utils';
1212
import Store from 'react-devtools-shared/src/devtools/store';
13+
import TimeoutError from 'react-devtools-shared/src/TimeoutError';
1314

1415
import type {
1516
InspectedElement as InspectedElementBackend,
@@ -102,6 +103,7 @@ export function inspectElement({
102103
requestID,
103104
'inspectedElement',
104105
bridge,
106+
`Timed out while inspecting element ${id}.`,
105107
);
106108

107109
bridge.send('inspectElement', {
@@ -144,6 +146,7 @@ function getPromiseForRequestID<T>(
144146
requestID: number,
145147
eventType: $Keys<BackendEvents>,
146148
bridge: FrontendBridge,
149+
timeoutMessage: string,
147150
): Promise<T> {
148151
return new Promise((resolve, reject) => {
149152
const cleanup = () => {
@@ -161,9 +164,7 @@ function getPromiseForRequestID<T>(
161164

162165
const onTimeout = () => {
163166
cleanup();
164-
reject(
165-
new Error(`Timed out waiting for event '${eventType}' from bridge`),
166-
);
167+
reject(new TimeoutError(timeoutMessage));
167168
};
168169

169170
bridge.addListener(eventType, onInspectedElement);

packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorBoundary.js

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import Store from 'react-devtools-shared/src/devtools/store';
1313
import ErrorView from './ErrorView';
1414
import SearchingGitHubIssues from './SearchingGitHubIssues';
1515
import SuspendingErrorView from './SuspendingErrorView';
16+
import TimeoutView from './TimeoutView';
17+
import TimeoutError from 'react-devtools-shared/src/TimeoutError';
1618

1719
type Props = {|
1820
children: React$Node,
@@ -27,6 +29,7 @@ type State = {|
2729
componentStack: string | null,
2830
errorMessage: string | null,
2931
hasError: boolean,
32+
isTimeout: boolean,
3033
|};
3134

3235
const InitialState: State = {
@@ -35,6 +38,7 @@ const InitialState: State = {
3538
componentStack: null,
3639
errorMessage: null,
3740
hasError: false,
41+
isTimeout: false,
3842
};
3943

4044
export default class ErrorBoundary extends Component<Props, State> {
@@ -48,6 +52,8 @@ export default class ErrorBoundary extends Component<Props, State> {
4852
? error.message
4953
: String(error);
5054

55+
const isTimeout = error instanceof TimeoutError;
56+
5157
const callStack =
5258
typeof error === 'object' &&
5359
error !== null &&
@@ -62,6 +68,7 @@ export default class ErrorBoundary extends Component<Props, State> {
6268
callStack,
6369
errorMessage,
6470
hasError: true,
71+
isTimeout,
6572
};
6673
}
6774

@@ -93,26 +100,40 @@ export default class ErrorBoundary extends Component<Props, State> {
93100
componentStack,
94101
errorMessage,
95102
hasError,
103+
isTimeout,
96104
} = this.state;
97105

98106
if (hasError) {
99-
return (
100-
<ErrorView
101-
callStack={callStack}
102-
componentStack={componentStack}
103-
dismissError={
104-
canDismissProp || canDismissState ? this._dismissError : null
105-
}
106-
errorMessage={errorMessage}>
107-
<Suspense fallback={<SearchingGitHubIssues />}>
108-
<SuspendingErrorView
109-
callStack={callStack}
110-
componentStack={componentStack}
111-
errorMessage={errorMessage}
112-
/>
113-
</Suspense>
114-
</ErrorView>
115-
);
107+
if (isTimeout) {
108+
return (
109+
<TimeoutView
110+
callStack={callStack}
111+
componentStack={componentStack}
112+
dismissError={
113+
canDismissProp || canDismissState ? this._dismissError : null
114+
}
115+
errorMessage={errorMessage}
116+
/>
117+
);
118+
} else {
119+
return (
120+
<ErrorView
121+
callStack={callStack}
122+
componentStack={componentStack}
123+
dismissError={
124+
canDismissProp || canDismissState ? this._dismissError : null
125+
}
126+
errorMessage={errorMessage}>
127+
<Suspense fallback={<SearchingGitHubIssues />}>
128+
<SuspendingErrorView
129+
callStack={callStack}
130+
componentStack={componentStack}
131+
errorMessage={errorMessage}
132+
/>
133+
</Suspense>
134+
</ErrorView>
135+
);
136+
}
116137
}
117138

118139
return children;

packages/react-devtools-shared/src/devtools/views/ErrorBoundary/ErrorView.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export default function ErrorView({
3232
{children}
3333
<div className={styles.ErrorInfo}>
3434
<div className={styles.HeaderRow}>
35-
<div className={styles.Header}>
35+
<div className={styles.ErrorHeader}>
3636
Uncaught Error: {errorMessage || ''}
3737
</div>
3838
{dismissError !== null && (
@@ -43,12 +43,12 @@ export default function ErrorView({
4343
)}
4444
</div>
4545
{!!callStack && (
46-
<div className={styles.Stack}>
46+
<div className={styles.ErrorStack}>
4747
The error was thrown {callStack.trim()}
4848
</div>
4949
)}
5050
{!!componentStack && (
51-
<div className={styles.Stack}>
51+
<div className={styles.ErrorStack}>
5252
The error occurred {componentStack.trim()}
5353
</div>
5454
)}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import * as React from 'react';
11+
import Button from '../Button';
12+
import ButtonIcon from '../ButtonIcon';
13+
import styles from './shared.css';
14+
15+
type Props = {|
16+
callStack: string | null,
17+
children: React$Node,
18+
componentStack: string | null,
19+
dismissError: Function,
20+
errorMessage: string | null,
21+
|};
22+
23+
export default function TimeoutView({
24+
callStack,
25+
children,
26+
componentStack,
27+
dismissError = null,
28+
errorMessage,
29+
}: Props) {
30+
return (
31+
<div className={styles.ErrorBoundary}>
32+
{children}
33+
<div className={styles.ErrorInfo}>
34+
<div className={styles.HeaderRow}>
35+
<div className={styles.TimeoutHeader}>
36+
{errorMessage || 'Timed out waiting'}
37+
</div>
38+
<Button className={styles.CloseButton} onClick={dismissError}>
39+
Retry
40+
<ButtonIcon className={styles.CloseButtonIcon} type="close" />
41+
</Button>
42+
</div>
43+
{!!componentStack && (
44+
<div className={styles.TimeoutStack}>
45+
The timeout occurred {componentStack.trim()}
46+
</div>
47+
)}
48+
</div>
49+
</div>
50+
);
51+
}

packages/react-devtools-shared/src/devtools/views/ErrorBoundary/shared.css

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
background-color: var(--color-background);
3131
display: flex;
3232
flex-direction: column;
33+
border: 1px solid var(--color-border);
3334
}
3435

3536
.ErrorInfo {
@@ -42,28 +43,46 @@
4243
flex-direction: row;
4344
font-size: var(--font-size-sans-large);
4445
font-weight: bold;
45-
color: var(--color-error-text);
4646
}
4747

48-
.Header {
48+
.ErrorHeader,
49+
.TimeoutHeader {
4950
flex: 1 1 auto;
5051
overflow: hidden;
5152
text-overflow: ellipsis;
5253
white-space: nowrap;
5354
min-width: 0;
5455
}
5556

56-
.Stack {
57+
.ErrorHeader {
58+
color: var(--color-error-text);
59+
}
60+
.TimeoutHeader {
61+
color: var(--color-text);
62+
}
63+
64+
.ErrorStack,
65+
.TimeoutStack {
5766
margin-top: 0.5rem;
5867
white-space: pre-wrap;
5968
font-family: var(--font-family-monospace);
6069
font-size: var(--font-size-monospace-normal);
6170
-webkit-font-smoothing: initial;
71+
border-radius: 0.25rem;
72+
padding: 0.5rem;
73+
overflow: auto;
74+
}
75+
76+
.ErrorStack {
6277
background-color: var(--color-error-background);
6378
border: 1px solid var(--color-error-border);
6479
color: var(--color-error-text);
65-
border-radius: 0.25rem;
66-
padding: 0.5rem;
80+
}
81+
82+
.TimeoutStack {
83+
background-color: var(--color-console-warning-background);
84+
color: var(--color-console-warning-text);
85+
border: var(--color-console-warning-border)
6786
}
6887

6988
.LoadingIcon {

packages/react-devtools-shared/src/inspectedElementCache.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type ResolvedRecord<T> = {|
3838

3939
type RejectedRecord = {|
4040
status: 2,
41-
value: string,
41+
value: Error | string,
4242
|};
4343

4444
type Record<T> = PendingRecord | ResolvedRecord<T> | RejectedRecord;
@@ -113,7 +113,9 @@ export function inspectElement(
113113
if (rendererID == null) {
114114
const rejectedRecord = ((newRecord: any): RejectedRecord);
115115
rejectedRecord.status = Rejected;
116-
rejectedRecord.value = `Could not inspect element with id "${element.id}". No renderer found.`;
116+
rejectedRecord.value = new Error(
117+
`Could not inspect element with id "${element.id}". No renderer found.`,
118+
);
117119

118120
map.set(element, record);
119121

@@ -139,7 +141,7 @@ export function inspectElement(
139141

140142
const rejectedRecord = ((newRecord: any): RejectedRecord);
141143
rejectedRecord.status = Rejected;
142-
rejectedRecord.value = `Could not inspect element with id "${element.id}". Error thrown:\n${error.message}`;
144+
rejectedRecord.value = error;
143145

144146
wake();
145147
},

0 commit comments

Comments
 (0)