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.