The Error
You open the browser and React slaps you with this:
Objects are not valid as a React child (found: object with keys {then}).
If you meant to render a collection of children, use an array instead.
That {then} in the message is the giveaway. React received a Promise object โ not a string, not a number, not JSX. Promises have a .then() method, and that's exactly how React identified what you passed it.
Why This Happens
Somewhere in your code, an async function call or a Promise ended up directly inside JSX โ without being awaited first. It's an easy mistake. Here are the three most common ways it happens:
Calling an async function directly in JSX
// โ fetchUserName() is async โ it returns a Promise, not a string
function UserCard() {
return (
<div>
<p>{fetchUserName()}</p>
</div>
);
}
Forgetting to await inside useEffect
// โ setData receives the Promise itself, not the resolved JSON
const [data, setData] = useState(null);
useEffect(() => {
setData(fetch('/api/data').then(r => r.json())); // Missing await!
}, []);
return <div>{data}</div>;
Using .map() with an async callback
// โ async callbacks inside .map() return an array of Promises
const items = ids.map(async (id) => {
const res = await fetch(`/api/item/${id}`);
return <li key={id}>{await res.json()}</li>;
});
return <ul>{items}</ul>;
Step-by-Step Fix
Step 1: Find where the Promise is leaking into JSX
Scan your JSX for any {} expression that calls an async function or returns a Promise. Three places to check:
- Inline calls like
{getUser()}wheregetUseris async - State variables set to a Promise rather than a resolved value
.map()callbacks withasyncin the signature
Step 2: Move async logic out of JSX โ useState + useEffect
The standard fix is straightforward. Fetch data inside useEffect, store the resolved value in state, then render that state variable.
import { useState, useEffect } from 'react';
function UserCard({ userId }) {
const [userName, setUserName] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
async function loadUser() {
const res = await fetch(`/api/users/${userId}`);
const data = await res.json();
setUserName(data.name); // โ
Resolved string, not a Promise
setLoading(false);
}
loadUser();
}, [userId]);
if (loading) return <p>Loading...</p>;
return (
<div>
<p>{userName}</p> {/* โ
A plain string */}
</div>
);
}
Step 3: Fix async .map() with Promise.all
Need to fetch data for multiple items? Resolve everything first with Promise.all, then store the results in state. Don't try to render mid-fetch.
// โ
Resolve all promises before storing in state
useEffect(() => {
async function loadItems() {
const results = await Promise.all(
ids.map(async (id) => {
const res = await fetch(`/api/item/${id}`);
return res.json();
})
);
setItems(results); // Array of plain objects
}
loadItems();
}, [ids]);
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li> {/* โ
Resolved data */}
))}
</ul>
);
Step 4: Don't mix .then() chains with setState
A subtle trap: calling setData() directly on a .then() chain. The chain returns a new Promise โ and that Promise goes straight into your state.
// โ setData receives a Promise, not the JSON
useEffect(() => {
setData(fetch('/api/data').then(r => r.json()));
}, []);
// โ
Define an async function inside useEffect instead
useEffect(() => {
async function load() {
const res = await fetch('/api/data');
const json = await res.json();
setData(json);
}
load();
}, []);
Step 5 (Next.js App Router): Server vs. Client Components
Next.js 13+ supports async Server Components โ but that async support does not extend to Client Components. If your file has 'use client' at the top, you cannot make the component function itself async. Move data fetching to a Server Component, or use useEffect inside the Client Component.
// โ
Server Component โ async works fine here
// app/user/[id]/page.tsx
export default async function UserPage({ params }) {
const res = await fetch(`https://api.example.com/users/${params.id}`);
const user = await res.json();
return <div>{user.name}</div>;
}
// โ Client Component โ async component function will break
'use client';
export default async function UserCard() { // Don't do this
const user = await getUser();
return <div>{user.name}</div>;
}
Verify the Fix
- Save and reload. The red error overlay in the browser should be gone.
- Open React DevTools and inspect your component's state โ you should see a string or plain object, not
Promise {<pending>}. - Drop a
console.logbefore the return to confirm what you're actually working with:
console.log('data is:', data, typeof data); // โ Should print: data is: { name: 'Alice' } object // โ Not: data is: Promise { } object
- In TypeScript, typed state catches this at compile time โ before it ever reaches the browser:
```
const [user, setUser] = useState<User | null>(null);
// TS will flag: setUser(fetchUser()) โ fetchUser() returns Promise<User>, not User
Quick Reference
- Symptom: Error says
object with keys {then}โ that's always a Promise. - Root cause: An async return value went directly into JSX or
setState()without being awaited. - Fix pattern:
useEffectโ async function inside โsetState(resolvedValue)โ render state. - TypeScript tip: Strict state types (
useState<string | null>) turn this runtime crash into a compile-time error. - Next.js tip: Async components are Server Component-only. Use
useEffectin Client Components.

