All posts
reacthooksdebugging

Missing return value in useEffect cleanup — What It Means and How to Fix It

"Missing return value in useEffect cleanup" explained — why it happens, a real code example that triggers it, and the exact fix.

SR

Suhail Roushan

August 6, 2026

·
3 min read
·
0 views

The "Missing return value in useEffect cleanup" warning fires in React's development mode when a useEffect callback returns a non-function value, signaling an invalid cleanup pattern.

What "Missing return value in useEffect cleanup" Means

React expects useEffect to return either nothing or a cleanup function. When you return something else—like a Promise, object, or boolean—React warns you because it can't properly tear down side effects between renders.

Why It Happens

The most common trigger is an async callback inside useEffect that returns a Promise instead of a cleanup function. React 18+ flags this explicitly because it can't await the Promise for cleanup. Another cause is forgetting to wrap logic in a function body with curly braces—a concise arrow () => result returns the value, while () => { } returns undefined. A third cause: accidentally returning the result of a subscription call like setInterval or a fetch call instead of the cleanup that cancels it.

Example Code That Triggers It

import { useEffect } from 'react';

function UserProfile({ userId }: { userId: string }) {
  useEffect(() => {
    // This returns a Promise — React can't use it for cleanup
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => console.log(data));
  }, [userId]);

  return <div>Profile</div>;
}

Run this in a browser with React 18+ and you'll see: Warning: Missing return value in useEffect cleanup. A cleanup function must be returned from the effect. The issue is the implicit return—the arrow function returns the fetch Promise chain.

How to Fix It

import { useEffect, useState } from 'react';

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState(null);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    let isMounted = true;
    
    const controller = new AbortController();
    
    fetch(`/api/users/${userId}`, { signal: controller.signal })
      .then(res => {
        if (!res.ok) throw new Error('Failed to fetch');
        return res.json();
      })
      .then(data => {
        if (isMounted) setUser(data);
      })
      .catch(err => {
        if (isMounted) setError(err.message);
      });

    // Return a cleanup function — cancels fetch and prevents state updates
    return () => {
      isMounted = false;
      controller.abort();
    };
  }, [userId]);

  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>Loading...</div>;
  return <div>{user.name}</div>;
}

The fix wraps the effect body in curly braces so nothing is implicitly returned. The explicit return () => { ... } gives React a real cleanup function that aborts the in-flight request and guards against setting state on unmounted components.

Common Mistakes That Cause This

1. Using async/await directly in the effect callback. useEffect(async () => { ... }) always returns a Promise. Wrap the async logic in an inner function and call it, then return a cleanup that sets a flag to ignore stale results.

2. Returning the result of a cleanup attempt. return clearInterval(intervalId) returns undefined (fine) but return intervalId returns a number—React will warn. Always return a function, not a value.

When Should You Worry About This?

You should worry immediately in React 18+ because it indicates a potential memory leak. Without a proper cleanup, subscriptions, timers, and network requests continue after unmounting, causing state updates on unmounted components and wasted resources. In React 19, this becomes a hard error, not just a warning. If you're on React 17 or earlier, it's still a latent bug—fix it before upgrading.

Next time this error appears, check your effect's arrow function syntax first: if it's concise (no braces), you're implicitly returning something you didn't intend.

Related posts

Written by Suhail Roushan — Full-stack developer. More posts on AI, Next.js, and building products at suhailroushan.com/blog.

Get in touch