Objects are not valid as a React child: what to render
Objects are not valid as a React child, and React tells you which one by listing its keys. In React 19 the promise version throws a different error.
React can render text, numbers, elements, and arrays of those. It cannot render a plain object, because there is no sensible way to turn { name: "Ada", age: 36 } into characters on screen. Asked to try, it throws:
const user = { name: "Ada", age: 36 };
return <div>{user}</div>;
Objects are not valid as a React child (found: object with keys {name, age}).
If you meant to render a collection of children, use an array instead.
The parenthetical is the useful part and it is often skipped. React lists the keys of the exact object it choked on, so in a page full of components you can identify which value is the problem without searching. {name, age} is your user object; {id, title} would be something else entirely.
Everything here was run against React 19.2.4.
What React will and will not render
| Value | Result |
|---|---|
"Ada", 42 |
Renders |
null, undefined, false |
Renders nothing, no error |
| An array of strings or elements | Renders |
| A plain object | Throws |
A Date |
Throws |
A Promise |
Throws, but see below |
The middle row is worth knowing because it is what makes conditional rendering work. {isLoggedIn && <Profile />} renders nothing when the condition is false, and that false is silently ignored rather than printed.
Arrays of primitives render with no separator at all:
<div>{["Ada", "Grace"]}</div>
// <div>AdaGrace</div>
That is rendering correctly, and it surprises people who expected commas. If you want a separator, put one there.
The fix is to pick a string out of the object
You almost never want to render the object. You want one of its fields:
<div>{user.name}</div>
When you genuinely want to display the whole thing, usually while debugging, stringify it:
<pre>{JSON.stringify(user, null, 2)}</pre>
JSON.stringify with those arguments gives readable, indented output. Wrapping it in <pre> keeps the indentation, since HTML would otherwise collapse it.
For a Date, the error reads (found: [object Date]), which is the same problem in different clothing. Dates are objects, so format them first:
<div>{createdAt.toLocaleDateString()}</div>
Arrays of objects: the same error, one level down
This is the most common real-world version:
const users = [{ name: "Ada" }, { name: "Grace" }];
return <div>{users}</div>;
Objects are not valid as a React child (found: object with keys {name}).
Read the keys again. The message says {name}, not {0, 1}. React unwrapped the array happily, then hit the first object inside it. The array was never the problem, which is why "use an array instead" in the error text is unhelpful here.
Map each object to something renderable:
{users.map((u) => (
<li key={u.name}>{u.name}</li>
))}
Note the key. Any list built this way needs one, and leaving it out produces a separate warning covered in the unique key prop warning.
The error object case
The version most likely to appear in production code, because it happens in the branch you tested least:
{error && <div className="error">{error}</div>}
If error holds a caught Error rather than a string, that throws:
Objects are not valid as a React child (found: [object Error]).
An Error is an object. Render its message:
{error && <div className="error">{error.message}</div>}
This is worth guarding deliberately, because your error path is the one place where a crash is least welcome. A component that throws while displaying an error tells the user nothing about what actually went wrong. If the value could be either a string or an Error, normalise it where you catch it, not where you render it.
The exact behaviour of the awkward types
React's rules here are less uniform than they look, and all of these were run rather than assumed:
| Value | What happens |
|---|---|
new Error("boom") |
Throws, (found: [object Error]) |
new Map([["a", 1]]) |
Renders a1, with a warning that Maps are not supported |
new Set([1, 2]) |
Renders 12, no warning at all |
10n (BigInt) |
Renders 10 |
Symbol("s") |
Renders nothing, warns that symbols are not valid |
[["a", "b"], "c"] |
Renders abc, nested arrays are flattened |
The Map and Set rows are the surprise. Both are objects, neither is a sensible thing to render, and React treats them differently: one warns and one is silent. Neither throws. So a Set that reached your JSX by mistake will render its contents jammed together and never tell you, which is a quieter failure than the error this page is about.
The practical lesson is that "React throws on objects" is not quite the rule. React throws on plain objects and a few specific types, and iterables slip through because they look enough like arrays.
The promise case changed in React 19
Every article on this error tells you that rendering a promise gives you "objects are not valid as a React child". In React 19 it does not. Rendering a promise produced this instead:
A component suspended while responding to synchronous input.
This will cause the UI to be replaced with a loading indicator.
To fix, updates that suspend should be wrapped with startTransition.
That is a completely different message pointing at a completely different subsystem. React 19 can unwrap promises through Suspense, so a promise child is no longer nonsense to it, and the failure is about when it suspended rather than what you passed.
The practical consequence is the same: you have an un-awaited async value where you wanted data. The usual cause is calling an async function in a component body and rendering the result:
// wrong: fetchUser() returns a promise
return <div>{fetchUser()}</div>;
Fetch in an effect and hold the result in state, or use a data-fetching library, or make it a server component that can await. What you should not do is search for "objects are not valid" and conclude your code is fine because the message does not match. On React 19 it will not.
Finding which value it is
The keys in the message are the fastest route, and they are enough on their own more often than people expect. {name, age} tells you it is a user; {message, stack} tells you it is an error; {current} tells you somebody rendered a ref.
When the keys are ambiguous because several values share a shape, the component name in the stack trace narrows it, and from there the JSX usually has only a handful of interpolations to check.
Three sources account for most of them:
An API returning a different shape than you expected. You wrote {user.name} and the endpoint returns { name: { first, last } }. The error keys tell you the real shape, which makes this error a useful signal about your data rather than a mistake in your markup.
A translation or config lookup that missed. Many i18n helpers return the whole object when a key path is incomplete, so t('nav') gives you every string under nav instead of one.
A ref rendered by mistake. {inputRef} instead of {inputRef.current.value} produces (found: object with keys {current}), which is a distinctive enough signature to recognise on sight.
Preventing it
TypeScript catches the whole class. ReactNode does not include plain objects, so <div>{user}</div> fails to compile when user is typed as an object. This is one of the clearest everyday wins from types in a React codebase.
Destructure props at the top of the component. A large share of these errors come from passing an object where a component expected a field, and naming the fields explicitly makes the mismatch obvious at the boundary rather than deep in the JSX.
Be suspicious of any value that came from JSON. An API returning { "name": { "first": "Ada" } } where you expected a string produces this error, and the keys in the message tell you the shape you actually received. That makes it a useful signal about your data, not just a mistake in your markup.
Want to build components where these failures show up immediately? Start with the React track.
More from the blog
npm install errors: read the code, not the stack
Every npm ERR code names one cause. ERESOLVE, E404, EJSONPARSE and the rest, with what each one means, measured against real installs.
Read moreUnexpected token in JavaScript: the two different errors
Unexpected token is two different errors. One is a typo in your file, the other means you got HTML where you expected JSON.
Read moreReady to write some code?
Put this into practice - start your first free lesson. No setup, no credit card.