Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. Objects are not valid as a React child: what to render
Tools

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.

By Max Arthur
Co-Founder & Content Marketer·October 7, 2026·7 Min read
White text reading 'Objects are not valid as a React child' on a dark purple dotted background

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.

Keep reading

More from the blog

White text reading 'How to fix npm install errors' on a dark purple dotted background
October 6, 2026·8 min readTools

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 more
White text reading 'Unexpected token in JavaScript' on a dark purple dotted background
October 4, 2026·7 min readTools

Unexpected 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 more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons