React Server Components (RSCs) are transforming how we build web applications by enabling server-side rendering without sending large JavaScript bundles to the client. However, like any React component, RSCs can throw errors that need to be handled appropriately. Let’s explore how to handle errors gracefully and implement retry mechanisms in React Server Components.
Understanding Error Boundaries
Error boundaries are React components that catch JavaScript errors anywhere in their child component tree. Instead of crashing your entire application, they allow you to:
- Catch errors during rendering
- Log those errors
- Display a fallback UI
Here’s a basic implementation of an error boundary:
class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false }
}
componentDidCatch(error, errorInfo) {
this.setState({ hasError: true })
console.error(error, errorInfo)
}
render() {
if (this.state.hasError) {
return <div>Something went wrong.</div>
}
return this.props.children
}
}
Using react-error-boundary
For a more robust solution, you can use the react-error-boundary
library. Here’s how to implement it in an RSC page:
'use client'
import { ErrorBoundary } from 'react-error-boundary'
export default function PageWithBoundary() {
return (
<>
<p>
This page demonstrates error handling with an explicit error boundary.
</p>
<ErrorBoundary fallback={<ErrorFallback />}>
<ErrorComponent />
</ErrorBoundary>
</>
)
}
function ErrorFallback() {
return (
<div className="text-red-700">
There was an error with this content
</div>
)
}
Implementing Retry Mechanisms
Instead of just showing an error message, we can give users the ability to retry failed operations. Here’s how to implement a retry mechanism:
'use client'
import { startTransition, useState } from 'react'
import { useRouter } from 'next/navigation'
export default function ErrorFallback({
error,
resetErrorBoundary,
}) {
const router = useRouter()
const [isResetting, setIsResetting] = useState(false)
function retry() {
setIsResetting(true)
startTransition(() => {
router.refresh()
resetErrorBoundary()
setIsResetting(false)
})
}
return (
<div className="border border-orange-700 p-4 text-orange-700">
<p className="m-0 mb-2 p-0">
There was an error loading this component
</p>
<button
onClick={() => retry()}
disabled={isResetting}
className="button inline-flex items-center gap-4 rounded-md border bg-blue-500 px-4 py-2 text-white hover:bg-blue-600"
>
{isResetting ? <Spinner /> : null}
Retry
</button>
</div>
)
}
Some key points about this implementation:
- The ErrorFallback component must be a client component
- We use
router.refresh()
to retry rendering - The retry operation is wrapped in
startTransition
sincerouter.refresh()
is a long-running operation - A loading state provides visual feedback during retry
Best Practices
When implementing error handling in RSCs:
- Keep error boundaries as close as possible to potential error sources
- Provide clear feedback to users when errors occur
- Implement retry mechanisms where appropriate
- Preserve user state and input during retries
- Use loading states to indicate retry operations
Conclusion
Proper error handling is crucial for building robust React applications. By combining error boundaries with retry mechanisms, you can create a more resilient application that recovers gracefully from errors while maintaining a great user experience.
Remember that while it might seem like we’re only retrying specific components, we’re actually refreshing the entire page. However, from the user’s perspective, it appears as a seamless retry of just the failed component, which is exactly what we want.