Forgetting to wrap asynchronous calls in try/catch

Loading

Proper Error Handling for Asynchronous Operations in React

Forgetting to wrap asynchronous calls in try/catch blocks is a common mistake that can lead to uncaught promise rejections and poor error handling in your React applications. Here’s how to properly handle asynchronous operations.

The Problem: Unhandled Promise Rejections

// ❌ Dangerous - uncaught errors
useEffect(() => {
  fetch('/api/data')
    .then(response => response.json())
    .then(data => setData(data));
}, []);

Correct Approaches

1. Basic try/catch with async/await

// ✅ Proper error handling
useEffect(() => {
  const fetchData = async () => {
    try {
      const response = await fetch('/api/data');
      if (!response.ok) throw new Error('Network response was not ok');
      const data = await response.json();
      setData(data);
    } catch (error) {
      console.error('Fetch error:', error);
      setError(error.message);
    }
  };

  fetchData();
}, []);

2. Error Boundaries for Component-Level Errors

// ErrorBoundary.js
class ErrorBoundary extends React.Component {
  state = { hasError: false };

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, info) {
    logErrorToService(error, info.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return <FallbackUI />;
    }
    return this.props.children;
  }
}

// Usage
<ErrorBoundary>
  <MyComponent />
</ErrorBoundary>

3. Centralized Error Handling

// api.js
export async function fetchData() {
  try {
    const response = await fetch('/api/data');
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return await response.json();
  } catch (error) {
    throw error; // Re-throw for component handling
  }
}

// Component
useEffect(() => {
  const loadData = async () => {
    try {
      const data = await fetchData();
      setData(data);
    } catch (error) {
      setError(error.message);
    }
  };

  loadData();
}, []);

Best Practices

  1. Always wrap async operations in try/catch
  2. Handle both network errors and API errors (check response.ok)
  3. Provide user feedback for failed operations
  4. Log errors to your error tracking service
  5. Consider retry mechanisms for transient failures
// With retry logic
const fetchWithRetry = async (url, retries = 3) => {
  try {
    const response = await fetch(url);
    if (!response.ok) throw new Error('Bad response');
    return await response.json();
  } catch (error) {
    if (retries <= 0) throw error;
    await new Promise(resolve => setTimeout(resolve, 1000));
    return fetchWithRetry(url, retries - 1);
  }
};

Common Async Patterns in React

1. Data Fetching in Components

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const fetchUser = async () => {
      setLoading(true);
      try {
        const response = await fetch(`/api/users/${userId}`);
        if (!response.ok) throw new Error('User not found');
        setUser(await response.json());
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]);

  if (loading) return <Loader />;
  if (error) return <Error message={error} />;
  return <Profile user={user} />;
}

2. Form Submission Handling

async function handleSubmit(event) {
  event.preventDefault();
  setSubmitting(true);

  try {
    const response = await fetch('/api/submit', {
      method: 'POST',
      body: JSON.stringify(formData),
      headers: { 'Content-Type': 'application/json' }
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.message || 'Submission failed');
    }

    const result = await response.json();
    onSuccess(result);
  } catch (error) {
    setSubmissionError(error.message);
  } finally {
    setSubmitting(false);
  }
}

Advanced Error Handling Techniques

1. Custom Hook for Async Operations

function useAsync(asyncFn, immediate = true) {
  const [status, setStatus] = useState('idle');
  const [value, setValue] = useState(null);
  const [error, setError] = useState(null);

  const execute = useCallback(async (...args) => {
    setStatus('pending');
    try {
      const result = await asyncFn(...args);
      setValue(result);
      setStatus('success');
      return result;
    } catch (error) {
      setError(error);
      setStatus('error');
      throw error;
    }
  }, [asyncFn]);

  useEffect(() => {
    if (immediate) execute();
  }, [execute, immediate]);

  return { execute, status, value, error };
}

// Usage
const { status, value, error } = useAsync(() => fetchData(url));

2. Global Error Handling

// Error context
const ErrorContext = createContext();

function ErrorProvider({ children }) {
  const [error, setError] = useState(null);

  const handleError = useCallback((error) => {
    console.error(error);
    setError(error.message);
    setTimeout(() => setError(null), 5000);
  }, []);

  return (
    <ErrorContext.Provider value={{ error, handleError }}>
      {error && <Notification message={error} />}
      {children}
    </ErrorContext.Provider>
  );
}

// Usage in components
function UserComponent() {
  const { handleError } = useContext(ErrorContext);

  const fetchUser = async () => {
    try {
      // ... async operation
    } catch (error) {
      handleError(error);
    }
  };
}

Remember that proper error handling:

  • Prevents app crashes from uncaught exceptions
  • Provides better user experience with meaningful feedback
  • Helps with debugging by properly logging errors
  • Makes your application more robust and maintainable

Always wrap asynchronous operations in try/catch blocks and consider implementing multiple layers of error handling (component-level, context-level, and error boundaries) for comprehensive coverage.

Leave a Reply

Your email address will not be published. Required fields are marked *