This error occurs when a React Hook throws an error that isn't properly caught and handled, often due to async operations, improper error boundaries, or errors in custom hooks that propagate unexpectedly.
The "Hook returned an unexpected error" message indicates that an error was thrown from within a React Hook (either built-in or custom) but wasn't caught by proper error handling mechanisms. React Hooks operate within the component lifecycle and render phases, and errors thrown from hooks can crash your component tree if not handled correctly. This error commonly appears when async operations inside hooks (like useEffect or custom hooks) fail, when promises reject without proper catch handlers, or when error boundaries don't properly catch hook-related errors. Unlike errors thrown during render which React can catch with error boundaries, async errors in hooks require explicit handling. Understanding this error is crucial because it affects how you structure error handling in functional components. Error boundaries, which are class components implementing componentDidCatch or getDerivedStateFromError, don't automatically catch errors from event handlers or async code within hooks.
Wrap async code in useEffect or custom hooks with try-catch blocks to handle errors gracefully:
// Before - error not caught
useEffect(() => {
const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
};
fetchData();
}, []);
// After - error properly caught
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setData(data);
} catch (error) {
setError(error.message);
console.error('Failed to fetch data:', error);
}
};
fetchData();
}, []);For promise-based code, use .catch() method:
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(data => setData(data))
.catch(error => {
setError(error.message);
console.error('Fetch failed:', error);
});
}, []);Create custom hooks that return error state alongside data and loading states:
function useFetchData(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage in component
function MyComponent() {
const { data, loading, error } = useFetchData('/api/data');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return <div>{JSON.stringify(data)}</div>;
}Use the react-error-boundary library which provides useErrorHandler hook for async errors:
npm install react-error-boundaryImplement in your component:
import { ErrorBoundary, useErrorHandler } from 'react-error-boundary';
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 MyComponent() {
const [data, setData] = useState(null);
const handleError = useErrorHandler();
useEffect(() => {
fetch('/api/data')
.then(response => response.json())
.then(setData)
.catch(handleError); // Propagates error to ErrorBoundary
}, [handleError]);
return <div>{data && JSON.stringify(data)}</div>;
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<MyComponent />
</ErrorBoundary>
);
}Use cleanup functions and abort signals to prevent errors from unmounted components:
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const fetchData = async () => {
try {
const response = await fetch('/api/data', {
signal: controller.signal
});
const data = await response.json();
// Only update state if component is still mounted
if (isMounted) {
setData(data);
}
} catch (error) {
// Ignore abort errors
if (error.name !== 'AbortError' && isMounted) {
setError(error.message);
}
}
};
fetchData();
// Cleanup function
return () => {
isMounted = false;
controller.abort();
};
}, []);Wrap components using hooks with error boundaries to catch rendering errors:
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
return { hasError: true, error };
}
componentDidCatch(error, errorInfo) {
console.error('Error caught by boundary:', error, errorInfo);
// Log to error reporting service
}
render() {
if (this.state.hasError) {
return (
<div>
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<pre>{this.state.error?.toString()}</pre>
</details>
</div>
);
}
return this.props.children;
}
}
// Usage
function App() {
return (
<ErrorBoundary>
<ComponentWithHooks />
</ErrorBoundary>
);
}Event handlers in hooks need their own error handling since error boundaries don't catch them:
function MyComponent() {
const [error, setError] = useState(null);
const handleClick = async () => {
try {
const response = await fetch('/api/action', {
method: 'POST',
body: JSON.stringify({ action: 'submit' })
});
if (!response.ok) {
throw new Error('Action failed');
}
const result = await response.json();
// Handle success
} catch (err) {
setError(err.message);
// Optionally propagate to error boundary
// throw err;
}
};
return (
<div>
<button onClick={handleClick}>Submit</button>
{error && <div className="error">{error}</div>}
</div>
);
}Consider using data fetching libraries that handle errors automatically:
// Using React Query
import { useQuery } from '@tanstack/react-query';
function MyComponent() {
const { data, error, isLoading } = useQuery({
queryKey: ['userData'],
queryFn: async () => {
const response = await fetch('/api/user');
if (!response.ok) {
throw new Error('Failed to fetch user');
}
return response.json();
},
retry: 3,
onError: (error) => {
console.error('Query error:', error);
}
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return <div>{JSON.stringify(data)}</div>;
}
// Using SWR
import useSWR from 'swr';
const fetcher = async (url) => {
const response = await fetch(url);
if (!response.ok) {
throw new Error('API request failed');
}
return response.json();
};
function MyComponent() {
const { data, error } = useSWR('/api/data', fetcher, {
onError: (err) => console.error('SWR error:', err)
});
if (error) return <div>Failed to load</div>;
if (!data) return <div>Loading...</div>;
return <div>{JSON.stringify(data)}</div>;
}Error Boundary Limitations: React error boundaries are class components that catch errors during render, lifecycle methods, and constructors of the whole tree below them. However, they do NOT catch errors in event handlers, asynchronous code (setTimeout, promises), server-side rendering, or errors thrown in the error boundary itself. This is why explicit try-catch blocks are essential for async hook operations.
Custom Error Handler Hook Pattern: Create a reusable useErrorHandler hook that combines state management with error boundary integration:
function useErrorHandler() {
const [error, setError] = useState(null);
useEffect(() => {
if (error) {
throw error; // Propagates to nearest error boundary
}
}, [error]);
return setError;
}Development vs Production: In development mode, React shows the error overlay which can mask proper error boundary behavior. Test error handling in production builds to verify actual user experience.
Error Logging Best Practices: Always log errors to a monitoring service (Sentry, LogRocket, etc.) in componentDidCatch or error handlers. Include component stack traces and user context for debugging. Consider implementing a global error handler for uncaught promise rejections:
window.addEventListener('unhandledrejection', (event) => {
console.error('Unhandled promise rejection:', event.reason);
// Log to monitoring service
});TypeScript Typing: When using TypeScript, properly type your error states to distinguish between different error types and provide better type safety in error handlers.
Prop spreading could cause security issues
Prop spreading could cause security issues
Error: error:0308010C:digital envelope routines::unsupported
Error: error:0308010C:digital envelope routines::unsupported
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render.
React Hook useEffect placed inside a condition
Hook can only be called inside the body of a function component
Hook can only be called inside the body of a function component
Rollup failed to resolve import during build
How to fix "Rollup failed to resolve import" in React