The page goes blank and the console shows:
Uncaught Error: Objects are not valid as a React child (found: object with keys {id, name, email}). If you meant to render a collection of children, use an array instead.React was asked to put a plain JavaScript object between JSX tags, and it has no way to turn an object into DOM. The part in parentheses is the most useful clue: for plain objects it lists the keys, and for built-in objects it prints the type, as in (found: [object Date]) or (found: [object Error]). Production builds shorten this to Minified React error #31, with the found-value in the linked URL.
Quick fix checklist
- Read the keys in the message and search your JSX for the variable that has them.
- Render a property, not the object:
{user.name}, not{user}. - Format dates yourself:
{date.toLocaleDateString()}or anIntl.DateTimeFormat, never{new Date()}. - Render errors as
{error.message}. - Turn arrays of objects into elements with
items.map(item => <li key={item.id}>{item.name}</li>). - Check that components destructure props:
function Greeting({ name }), notfunction Greeting(name). - If the found value is a Promise or you see “async Client Component”, see the async section below.
Before you start
You need to know that JSX like <p>{value}</p> compiles to a call that passes value as a child, and that React decides how to display each child by its type. This guide covers React 19 and notes where React 18 behaves differently (mainly around promises). It is about the client and server renderer alike; the same message appears in tests and server-side rendering.
Why it happens
React accepts these values as children:
- strings and numbers, which become text (
0andNaNare rendered, so{count && <Badge />}prints0whencountis 0) - React elements, created by JSX
- arrays and other iterables of the above, rendered in order
null,undefined,trueandfalse, which render nothing- in React 19, promises and other thenables, which React unwraps by suspending (like the
useAPI)
Everything else that is an object makes React throw, because there is no sensible default. Should { id, name, email } show the name? All three fields? JSON? React refuses to guess. The hint about “a collection of children” simply means: if you have many things, put them in an array.
The usual ways an object ends up there:
// 1. The whole record instead of a field
<p>Customer: {order.customer}</p>
// 2. A Date, Error, Map entry or class instance
<p>Placed: {new Date(order.placedAt)}</p>
<p className="error">{error}</p>
// 3. Forgetting to destructure props: name is the props object { name: 'Ada' }
function Greeting(name) {
return <p>Hello {name}</p>;
}
// 4. Double braces from another template language: an object literal { count }
<p>{{ count }}</p>Case 3 produces (found: object with keys {name}) and is easy to miss because the code reads naturally. Case 4 is common for developers coming from Vue or Angular, where {{ }} interpolates; in JSX the outer braces open an expression and the inner ones create an object.
Another tell: keys starting with $$typeof (such as {$$typeof, render} for a forwardRef component) mean you rendered a component object without angle brackets: {Card} instead of <Card />. If they look like _reactName, _targetInst, type, nativeEvent..., an event object was stored in state, usually from onChange={setValue} instead of onChange={(e) => setValue(e.target.value)}.
Promises and async components
In React 18, rendering a promise threw this same error with (found: [object Promise]). React 19 treats a promise child as something to wait for: it suspends until the promise resolves. If the promise is created during render, such as <p>{fetchName()}</p>, each render creates a new one and the nearest <Suspense> fallback stays on screen forever.
An async function component returns a promise too. In React 19 on the client, that produces a different message:
<Profile> is an async Client Component. Only Server Components can be async at the moment. This error is often caused by accidentally adding `'use client'` to a module that was originally written for the server.In React 18 the same component crashed with Objects are not valid as a React child (found: [object Promise]). Async components are only supported as Server Components (for example in the Next.js App Router), so if you added 'use client' to an async component or render it from a client-only app, restructure the data loading instead.
Step-by-step walkthrough
Step 1: Read the found value
Copy the text in parentheses. object with keys {id, name, email} is a customer record; {name} alone is probably props; [object Date] is a date. React 19 also logs An error occurred in the <OrderSummary> component, telling you which file to open. If you only have Minified React error #31, open the linked react.dev URL; it shows the decoded message with the found value.
Step 2: Find the expression
In that component, look at every { } between tags and ask what type it evaluates to. Temporary logging helps when data comes from an API:
console.log('customer is', typeof order.customer, order.customer);If the shape is unclear, render it as JSON for a moment: <pre>{JSON.stringify(order, null, 2)}</pre>. A string is a valid child, so this never throws.
Step 3: Render text or elements
Pick what the user should see and convert it explicitly:
<p>Customer: {order.customer.name}</p>
<p className="error">{error.message}</p>
<p>Tags: {tags.join(', ')}</p>
<ul>
{Object.entries(settings).map(([key, value]) => (
<li key={key}>{key}: {String(value)}</li>
))}
</ul>For dates, choose a format and a time zone deliberately. Intl.DateTimeFormat is built into every modern browser and Node.js.
Step 4: Move async work out of render
For async components on the client, load data outside the component body. In a framework with Server Components, keep the component async and remove 'use client'. In a client app, use a data library, an effect, or create the promise outside render (in a parent, a loader or a cache) and read it with use inside <Suspense>:
import { Suspense, use } from 'react';
function Profile({ userPromise }) {
const user = use(userPromise);
return <p>{user.name}</p>;
}
// The promise is created once, outside render, e.g. in a route loader or event handler
<Suspense fallback={<p>Loading...</p>}>
<Profile userPromise={userPromise} />
</Suspense>Worked scenario
An order page renders data from /api/orders/A-1042:
function OrderSummary({ order }) {
return (
<section>
<h2>Order {order.id}</h2>
<p>Customer: {order.customer}</p>
<p>Placed: {new Date(order.placedAt)}</p>
</section>
);
}The first error reads (found: object with keys {id, name, email}). Those are the fields of order.customer, so line two is the culprit. After changing it to {order.customer.name}, a second error appears: (found: [object Date]). Multiple bad children are reported one at a time, so expect to fix them in sequence.
const placedFormat = new Intl.DateTimeFormat('en-GB', {
dateStyle: 'medium',
timeStyle: 'short',
timeZone: 'UTC',
});
function OrderSummary({ order }) {
return (
<section>
<h2>Order {order.id}</h2>
<p>Customer: {order.customer.name}</p>
<p>
Placed: <time dateTime={order.placedAt}>{placedFormat.format(new Date(order.placedAt))}</time>
</p>
<ul>
{order.items.map((item) => (
<li key={item.sku}>{item.sku} x {item.qty}</li>
))}
</ul>
</section>
);
}With placedAt: '2026-10-01T09:30:00Z' this renders Placed: 1 Oct 2026, 09:30. The formatter is created once at module level because constructing Intl.DateTimeFormat is comparatively expensive, and the explicit timeZone keeps server and client output identical, which avoids hydration mismatches.
Common mistake
The quick fix people reach for is {JSON.stringify(order.customer)} or {String(order.customer)}. The first silences the error but shows {"id":7,"name":"Ada Lovelace","email":"ada@example.com"} to users, leaking fields you did not mean to display. The second shows [object Object]. Template literals ({`${user}`}) have the same [object Object] problem. Use JSON.stringify only as a debugging aid inside <pre>.
A second tempting fix for async components is wrapping the call in useEffect but keeping async on the component itself. The component still returns a promise, so the error stays. Remove async from the component, and if you load data in an effect, guard against out-of-order responses (see fetch race conditions).
Verify the behavior
A component test that renders with realistic API data catches regressions when the backend changes a string field into an object:
import { render, screen } from '@testing-library/react';
test('renders customer name and formatted date', () => {
const order = {
id: 'A-1042',
customer: { id: 7, name: 'Ada Lovelace', email: 'ada@example.com' },
placedAt: '2026-10-01T09:30:00Z',
items: [{ sku: 'KB-1', qty: 1 }],
};
render(<OrderSummary order={order} />);
expect(screen.getByText('Customer: Ada Lovelace')).toBeInTheDocument();
expect(screen.getByText('1 Oct 2026, 09:30')).toBeInTheDocument();
});Before the fix, render throws with the keys message; after it, the test passes. With TypeScript, typing order.customer as an object makes {order.customer} a compile error (Type '{ ... }' is not assignable to type 'ReactNode'), which is the cheapest prevention.
Interview exercise
What is the difference between {items} and {items.map(i => i)} when items is [{ id: 1, label: 'A' }], and why does React accept an array but reject an object?
Answer and reasoning
Both throw, with (found: object with keys {id, label}). React does accept arrays, but it renders each element as a child, and each element here is still a plain object. The array is fine; its contents are not. The fix is mapping to elements: items.map(i => <li key={i.id}>{i.label}</li>). React accepts arrays because their meaning is unambiguous (render these children in order, matched across renders by key), while an object has no defined order of display or meaning: React would have to guess which fields matter. A good answer also mentions that React 19 added promises to the allowed child types (they suspend), which is why the same code that threw [object Promise] in React 18 now shows a Suspense fallback.
Continue learning
Practise with the React interview questions and the React MCQs. List keys and identity explains the key you need once you map objects to elements, and Suspense boundaries covers what happens when a child suspends. If the object came from storing an event in state, Too many re-renders explains event handler mistakes. Official references: Rendering Lists, use and Server Components on react.dev.