The simplest way to use <ErrorBoundary> is to wrap it around any
component that may throw an error. This will handle errors thrown by that
component and its descendants too.
import { ErrorBoundary } from "@blitzjs/next"
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
const ui = (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// reset the state of your app so the error doesn't happen again
}}
>
<ComponentThatMayError />
</ErrorBoundary>
)You can react to errors (e.g. for logging) by providing an onError
callback:
import { ErrorBoundary } from "@blitzjs/next"
const myErrorHandler = (
error: Error,
info: { componentStack: string }
) => {
// Do something with the error
// E.g. log to an error logging client here
}
const ui = (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onError={myErrorHandler}
>
<ComponentThatMayError />
</ErrorBoundary>
)You can also use it as a higher-order component:
import { withErrorBoundary } from "@blitzjs/next"
const ComponentWithErrorBoundary = withErrorBoundary(
ComponentThatMayError,
{
FallbackComponent: ErrorBoundaryFallbackComponent,
onError(error, info) {
// Do something with the error
// E.g. log to an error logging client here
},
}
)
const ui = <ComponentWithErrorBoundary />In the event of an error if you want to recover from that error and allow the user to "try again" or continue with their work, you'll need a way to reset the ErrorBoundary's internal state. You can do this various ways, but here's the most idiomatic approach:
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
)
}
function Bomb() {
throw new Error("💥 CABOOM 💥")
}
function App() {
const [explode, setExplode] = React.useState(false)
return (
<div>
<button onClick={() => setExplode((e) => !e)}>
toggle explode
</button>
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => setExplode(false)}
resetKeys={[explode]}
>
{explode ? <Bomb /> : null}
</ErrorBoundary>
</div>
)
}So, with this setup, you've got a button which when clicked will trigger
an error. Clicking the button again will trigger a re-render which
recovers from the error (we no longer render the <Bomb />). We also pass
the resetKeys prop which is an array of elements for the ErrorBoundary
to check each render (if there's currently an error state). If any of
those elements change between renders, then the ErrorBoundary will reset
the state which will re-render the children.
We have the onReset prop so that if the user clicks the "Try again"
button we have an opportunity to re-initialize our state into a good place
before attempting to re-render the children.
This combination allows us both the opportunity to give the user something specific to do to recover from the error, and recover from the error by interacting with other areas of the app that might fix things for us. It's hard to describe here, but hopefully it makes sense when you apply it to your specific scenario.
ErrorBoundary propschildrenThis is what you want rendered when everything's working fine. If there's
an error that React can handle within the children of the ErrorBoundary,
the ErrorBoundary will catch that and allow you to handle it gracefully.
FallbackComponentThis is a component you want rendered in the event of an error. As props
it will be passed the error and resetErrorBoundary (which will reset
the error boundary's state when called, useful for a "try again" button
when used in combination with the onReset prop).
This is required if no fallback or fallbackRender prop is provided.
fallbackRenderThis is a render-prop based API that allows you to inline your error
fallback UI into the component that's using the ErrorBoundary. This is
useful if you need access to something that's in the scope of the
component you're using.
It will be called with an object that has error and
resetErrorBoundary:
const ui = (
<ErrorBoundary
fallbackRender={({ error, resetErrorBoundary }) => (
<div role="alert">
<div>Oh no</div>
<pre>{error.message}</pre>
<button
onClick={() => {
// this next line is why the fallbackRender is useful
resetComponentState()
// though you could accomplish this with a combination
// of the FallbackCallback and onReset props as well.
resetErrorBoundary()
}}
>
Try again
</button>
</div>
)}
>
<ComponentThatMayError />
</ErrorBoundary>
)I know what you're thinking: I thought we ditched render props when hooks came around. Unfortunately, the current React Error Boundary API only supports class components at the moment, so render props are the best solution we have to this problem.
This is required if no FallbackComponent or fallback prop is provided.
fallbackIn the spirit of consistency with the React.Suspense component, we also
support a simple fallback prop which you can use for a generic fallback.
This will not be passed any props so you can't show the user anything
actually useful though, so it's not really recommended.
const ui = (
<ErrorBoundary fallback={<div>Oh no</div>}>
<ComponentThatMayError />
</ErrorBoundary>
)onErrorThis will be called when there's been an error that the ErrorBoundary
has handled. It will be called with two arguments: error, info.
onResetThis will be called immediately before the ErrorBoundary resets it's
internal state (which will result in rendering the children again). You
should use this to ensure that re-rendering the children will not result
in a repeat of the same error happening again.
onReset will be called with whatever resetErrorBoundary is called
with.
Important: onReset will not be called when reset happens from a
change in resetKeys. Use onResetKeysChange for that.
resetKeysSometimes an error happens as a result of local state to the component
that's rendering the error. If this is the case, then you can pass
resetKeys which is an array of values. If the ErrorBoundary is in an
error state, then it will check these values each render and if they
change from one render to the next, then it will reset automatically
(triggering a re-render of the children).
We automatically reset any time the route changes. We effectively use
resetKeys={[router.asPath]} internally.
See the recovery examples above.
onResetKeysChangeThis is called when the resetKeys are changed (triggering a reset of the
ErrorBoundary). It's called with the prevResetKeys and the
resetKeys.
useErrorHandler(error?: unknown)React's error boundaries feature is limited in that the boundaries can only handle errors thrown during React's lifecycles. To quote the React docs on Error Boundaries:
Error boundaries do not catch errors for:
- Event handlers (learn more)
- Asynchronous code (e.g. setTimeout or requestAnimationFrame callbacks)
- Server side rendering
- Errors thrown in the error boundary itself (rather than its children)
This means you have to handle those errors yourself, but you probably
would like to reuse the error boundaries you worked hard on creating for
those kinds of errors as well. This is what useErrorHandler is for.
There are two ways to use useErrorHandler:
const handleError = useErrorHandler(): call handleError(theError)useErrorHandler(error): useful if you are managing the error state
yourself or get it from another hook.Here's an example:
function Greeting() {
const [greeting, setGreeting] = React.useState(null)
const handleError = useErrorHandler()
function handleSubmit(event) {
event.preventDefault()
const name = event.target.elements.name.value
fetchGreeting(name).then(
(newGreeting) => setGreeting(newGreeting),
handleError
)
}
return greeting ? (
<div>{greeting}</div>
) : (
<form onSubmit={handleSubmit}>
<label>Name</label>
<input id="name" />
<button type="submit">get a greeting</button>
</form>
)
}Note, in case it's not clear what's happening here, you could also write
handleSubmitlike this:
function handleSubmit(event) {
event.preventDefault()
const name = event.target.elements.name.value
fetchGreeting(name).then(
(newGreeting) => setGreeting(newGreeting),
(error) => handleError(error)
)
}Alternatively, let's say you're using a hook that gives you the error:
function Greeting() {
const [name, setName] = React.useState("")
const { greeting, error } = useGreeting(name)
useErrorHandler(error)
function handleSubmit(event) {
event.preventDefault()
const name = event.target.elements.name.value
setName(name)
}
return greeting ? (
<div>{greeting}</div>
) : (
<form onSubmit={handleSubmit}>
<label>Name</label>
<input id="name" />
<button type="submit">get a greeting</button>
</form>
)
}In this case, if the error is ever set to a truthy value, then it will
be propagated to the nearest error boundary.
In either case, you could handle those errors like this:
const ui = (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Greeting />
</ErrorBoundary>
)And now that'll handle your runtime errors as well as the async errors in
the fetchGreeting or useGreeting code.