Skip to content

Commit

Permalink
polish(theme): better error messages on navbar item rendering failure…
Browse files Browse the repository at this point in the history
…s + ErrorCauseBoundary API (#8735)

Co-authored-by: sebastienlorber <[email protected]>
  • Loading branch information
slorber committed Mar 23, 2023
1 parent 6deecd7 commit 4b37c01
Show file tree
Hide file tree
Showing 9 changed files with 98 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import React, {type ReactNode} from 'react';
import {useThemeConfig} from '@docusaurus/theme-common';
import {useThemeConfig, ErrorCauseBoundary} from '@docusaurus/theme-common';
import {
splitNavbarItems,
useNavbarMobileSidebar,
Expand All @@ -29,7 +29,18 @@ function NavbarItems({items}: {items: NavbarItemConfig[]}): JSX.Element {
return (
<>
{items.map((item, i) => (
<NavbarItem {...item} key={i} />
<ErrorCauseBoundary
key={i}
onError={(error) =>
new Error(
`A theme navbar item failed to render.
Please double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:
${JSON.stringify(item, null, 2)}`,
{cause: error},
)
}>
<NavbarItem {...item} />
</ErrorCauseBoundary>
))}
</>
);
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-theme-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@docusaurus/plugin-content-docs": "2.3.1",
"@docusaurus/plugin-content-pages": "2.3.1",
"@docusaurus/utils": "2.3.1",
"@docusaurus/utils-common": "2.3.1",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
Expand Down
1 change: 1 addition & 0 deletions packages/docusaurus-theme-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,5 @@ export {
export {
ErrorBoundaryTryAgainButton,
ErrorBoundaryError,
ErrorCauseBoundary,
} from './utils/errorBoundaryUtils';
8 changes: 4 additions & 4 deletions packages/docusaurus-theme-common/src/utils/docsUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,8 +271,8 @@ export function useLayoutDocsSidebar(
`Can't find any sidebar with id "${sidebarId}" in version${
versions.length > 1 ? 's' : ''
} ${versions.map((version) => version.name).join(', ')}".
Available sidebar ids are:
- ${Object.keys(allSidebars).join('\n- ')}`,
Available sidebar ids are:
- ${Object.keys(allSidebars).join('\n- ')}`,
);
}
return sidebarEntry[1];
Expand Down Expand Up @@ -304,9 +304,9 @@ export function useLayoutDoc(
return null;
}
throw new Error(
`DocNavbarItem: couldn't find any doc with id "${docId}" in version${
`Couldn't find any doc with id "${docId}" in version${
versions.length > 1 ? 's' : ''
} ${versions.map((version) => version.name).join(', ')}".
} "${versions.map((version) => version.name).join(', ')}".
Available doc ids are:
- ${uniq(allDocs.map((versionDoc) => versionDoc.id)).join('\n- ')}`,
);
Expand Down
32 changes: 30 additions & 2 deletions packages/docusaurus-theme-common/src/utils/errorBoundaryUtils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

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

export function ErrorBoundaryTryAgainButton(
Expand All @@ -22,7 +23,34 @@ export function ErrorBoundaryTryAgainButton(
</button>
);
}

export function ErrorBoundaryError({error}: {error: Error}): JSX.Element {
return <p className={styles.errorBoundaryError}>{error.message}</p>;
const causalChain = getErrorCausalChain(error);
const fullMessage = causalChain.map((e) => e.message).join('\n\nCause:\n');
return <p className={styles.errorBoundaryError}>{fullMessage}</p>;
}

/**
* This component is useful to wrap a low-level error into a more meaningful
* error with extra context, using the ES error-cause feature.
*
* <ErrorCauseBoundary
* onError={(error) => new Error("extra context message",{cause: error})}
* >
* <RiskyComponent>
* </ErrorCauseBoundary>
*/
export class ErrorCauseBoundary extends React.Component<
{
children: React.ReactNode;
onError: (error: Error, errorInfo: React.ErrorInfo) => Error;
},
unknown
> {
override componentDidCatch(error: Error, errorInfo: React.ErrorInfo): never {
throw this.props.onError(error, errorInfo);
}

override render(): React.ReactNode {
return this.props.children;
}
}
26 changes: 26 additions & 0 deletions packages/docusaurus-utils-common/src/__tests__/errorUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {getErrorCausalChain} from '../errorUtils';

describe('getErrorCausalChain', () => {
it('works for simple error', () => {
const error = new Error('msg');
expect(getErrorCausalChain(error)).toEqual([error]);
});

it('works for nested errors', () => {
const error = new Error('msg', {
cause: new Error('msg', {cause: new Error('msg')}),
});
expect(getErrorCausalChain(error)).toEqual([
error,
error.cause,
(error.cause as Error).cause,
]);
});
});
14 changes: 14 additions & 0 deletions packages/docusaurus-utils-common/src/errorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
type CausalChain = [Error, ...Error[]];

export function getErrorCausalChain(error: Error): CausalChain {
if (error.cause) {
return [error, ...getErrorCausalChain(error.cause as Error)];
}
return [error];
}
1 change: 1 addition & 0 deletions packages/docusaurus-utils-common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ export {
default as applyTrailingSlash,
type ApplyTrailingSlashParams,
} from './applyTrailingSlash';
export {getErrorCausalChain} from './errorUtils';
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import React from 'react';
import Head from '@docusaurus/Head';
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import {getErrorCausalChain} from '@docusaurus/utils-common';
import Layout from '@theme/Layout';
import type {Props} from '@theme/Error';

Expand Down Expand Up @@ -42,11 +43,17 @@ function ErrorDisplay({error, tryAgain}: Props): JSX.Element {
}}>
Try again
</button>
<p style={{whiteSpace: 'pre-wrap'}}>{error.message}</p>
<ErrorBoundaryError error={error} />
</div>
);
}

function ErrorBoundaryError({error}: {error: Error}): JSX.Element {
const causalChain = getErrorCausalChain(error);
const fullMessage = causalChain.map((e) => e.message).join('\n\nCause:\n');
return <p style={{whiteSpace: 'pre-wrap'}}>{fullMessage}</p>;
}

export default function Error({error, tryAgain}: Props): JSX.Element {
// We wrap the error in its own error boundary because the layout can actually
// throw too... Only the ErrorDisplay component is simple enough to be
Expand Down

0 comments on commit 4b37c01

Please sign in to comment.