Skip to content

Commit 4b37c01

Browse files
committed
polish(theme): better error messages on navbar item rendering failures + ErrorCauseBoundary API (#8735)
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
1 parent 6deecd7 commit 4b37c01

File tree

9 files changed

+98
-9
lines changed

9 files changed

+98
-9
lines changed

packages/docusaurus-theme-classic/src/theme/Navbar/Content/index.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import React, {type ReactNode} from 'react';
9-
import {useThemeConfig} from '@docusaurus/theme-common';
9+
import {useThemeConfig, ErrorCauseBoundary} from '@docusaurus/theme-common';
1010
import {
1111
splitNavbarItems,
1212
useNavbarMobileSidebar,
@@ -29,7 +29,18 @@ function NavbarItems({items}: {items: NavbarItemConfig[]}): JSX.Element {
2929
return (
3030
<>
3131
{items.map((item, i) => (
32-
<NavbarItem {...item} key={i} />
32+
<ErrorCauseBoundary
33+
key={i}
34+
onError={(error) =>
35+
new Error(
36+
`A theme navbar item failed to render.
37+
Please double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:
38+
${JSON.stringify(item, null, 2)}`,
39+
{cause: error},
40+
)
41+
}>
42+
<NavbarItem {...item} />
43+
</ErrorCauseBoundary>
3344
))}
3445
</>
3546
);

packages/docusaurus-theme-common/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"@docusaurus/plugin-content-docs": "2.3.1",
3737
"@docusaurus/plugin-content-pages": "2.3.1",
3838
"@docusaurus/utils": "2.3.1",
39+
"@docusaurus/utils-common": "2.3.1",
3940
"@types/history": "^4.7.11",
4041
"@types/react": "*",
4142
"@types/react-router-config": "*",

packages/docusaurus-theme-common/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,5 @@ export {
9393
export {
9494
ErrorBoundaryTryAgainButton,
9595
ErrorBoundaryError,
96+
ErrorCauseBoundary,
9697
} from './utils/errorBoundaryUtils';

packages/docusaurus-theme-common/src/utils/docsUtils.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,8 @@ export function useLayoutDocsSidebar(
271271
`Can't find any sidebar with id "${sidebarId}" in version${
272272
versions.length > 1 ? 's' : ''
273273
} ${versions.map((version) => version.name).join(', ')}".
274-
Available sidebar ids are:
275-
- ${Object.keys(allSidebars).join('\n- ')}`,
274+
Available sidebar ids are:
275+
- ${Object.keys(allSidebars).join('\n- ')}`,
276276
);
277277
}
278278
return sidebarEntry[1];
@@ -304,9 +304,9 @@ export function useLayoutDoc(
304304
return null;
305305
}
306306
throw new Error(
307-
`DocNavbarItem: couldn't find any doc with id "${docId}" in version${
307+
`Couldn't find any doc with id "${docId}" in version${
308308
versions.length > 1 ? 's' : ''
309-
} ${versions.map((version) => version.name).join(', ')}".
309+
} "${versions.map((version) => version.name).join(', ')}".
310310
Available doc ids are:
311311
- ${uniq(allDocs.map((versionDoc) => versionDoc.id)).join('\n- ')}`,
312312
);

packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import React, {type ComponentProps} from 'react';
99
import Translate from '@docusaurus/Translate';
10+
import {getErrorCausalChain} from '@docusaurus/utils-common';
1011
import styles from './errorBoundaryUtils.module.css';
1112

1213
export function ErrorBoundaryTryAgainButton(
@@ -22,7 +23,34 @@ export function ErrorBoundaryTryAgainButton(
2223
</button>
2324
);
2425
}
25-
2626
export function ErrorBoundaryError({error}: {error: Error}): JSX.Element {
27-
return <p className={styles.errorBoundaryError}>{error.message}</p>;
27+
const causalChain = getErrorCausalChain(error);
28+
const fullMessage = causalChain.map((e) => e.message).join('\n\nCause:\n');
29+
return <p className={styles.errorBoundaryError}>{fullMessage}</p>;
30+
}
31+
32+
/**
33+
* This component is useful to wrap a low-level error into a more meaningful
34+
* error with extra context, using the ES error-cause feature.
35+
*
36+
* <ErrorCauseBoundary
37+
* onError={(error) => new Error("extra context message",{cause: error})}
38+
* >
39+
* <RiskyComponent>
40+
* </ErrorCauseBoundary>
41+
*/
42+
export class ErrorCauseBoundary extends React.Component<
43+
{
44+
children: React.ReactNode;
45+
onError: (error: Error, errorInfo: React.ErrorInfo) => Error;
46+
},
47+
unknown
48+
> {
49+
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): never {
50+
throw this.props.onError(error, errorInfo);
51+
}
52+
53+
override render(): React.ReactNode {
54+
return this.props.children;
55+
}
2856
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
8+
import {getErrorCausalChain} from '../errorUtils';
9+
10+
describe('getErrorCausalChain', () => {
11+
it('works for simple error', () => {
12+
const error = new Error('msg');
13+
expect(getErrorCausalChain(error)).toEqual([error]);
14+
});
15+
16+
it('works for nested errors', () => {
17+
const error = new Error('msg', {
18+
cause: new Error('msg', {cause: new Error('msg')}),
19+
});
20+
expect(getErrorCausalChain(error)).toEqual([
21+
error,
22+
error.cause,
23+
(error.cause as Error).cause,
24+
]);
25+
});
26+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
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+
type CausalChain = [Error, ...Error[]];
8+
9+
export function getErrorCausalChain(error: Error): CausalChain {
10+
if (error.cause) {
11+
return [error, ...getErrorCausalChain(error.cause as Error)];
12+
}
13+
return [error];
14+
}

packages/docusaurus-utils-common/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ export {
1010
default as applyTrailingSlash,
1111
type ApplyTrailingSlashParams,
1212
} from './applyTrailingSlash';
13+
export {getErrorCausalChain} from './errorUtils';

packages/docusaurus/src/client/theme-fallback/Error/index.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import React from 'react';
1212
import Head from '@docusaurus/Head';
1313
import ErrorBoundary from '@docusaurus/ErrorBoundary';
14+
import {getErrorCausalChain} from '@docusaurus/utils-common';
1415
import Layout from '@theme/Layout';
1516
import type {Props} from '@theme/Error';
1617

@@ -42,11 +43,17 @@ function ErrorDisplay({error, tryAgain}: Props): JSX.Element {
4243
}}>
4344
Try again
4445
</button>
45-
<p style={{whiteSpace: 'pre-wrap'}}>{error.message}</p>
46+
<ErrorBoundaryError error={error} />
4647
</div>
4748
);
4849
}
4950

51+
function ErrorBoundaryError({error}: {error: Error}): JSX.Element {
52+
const causalChain = getErrorCausalChain(error);
53+
const fullMessage = causalChain.map((e) => e.message).join('\n\nCause:\n');
54+
return <p style={{whiteSpace: 'pre-wrap'}}>{fullMessage}</p>;
55+
}
56+
5057
export default function Error({error, tryAgain}: Props): JSX.Element {
5158
// We wrap the error in its own error boundary because the layout can actually
5259
// throw too... Only the ErrorDisplay component is simple enough to be

0 commit comments

Comments
 (0)