Fix this error · React and Next.js
Fix: Objects are not valid as a React child
React error #31 means JSX got an object, Date or error object. How to read the found keys, fix each cause, and catch it in production.
JSX received a plain object, a Date or another non-renderable value where it expected text, a number, an element or an array. React cannot guess how to display an object, so it throws. The fix is to render a field of the object ({user.name}), format the value as a string ({date.toLocaleDateString()}), or map an array to elements.
| Fact | Detail |
|---|---|
| Error | Objects are not valid as a React child (found: object with keys {id, name}). If you meant to render a collection of children, use an array instead. |
| In production builds | Minified React error #31 with the found value in the link, such as args[]=object%20with%20keys%20%7Bid%2C%20name%7D |
| Where you see it | Browser console, the Next.js error overlay, server logs when it happens during server rendering |
| Usual cause | {user} or {error} in JSX where you meant {user.name} or {error.message} |
| Usual fix | Render a string or number from the object, or map arrays to elements |
| BugTape catches it | Yes, automatically, when no error boundary of yours catches it. One line in your boundary otherwise |
What the error means
React can render strings, numbers, elements, arrays of those, and null, undefined or booleans (which render nothing). When a child is any other object, React throws error #31.
The text inside found: tells you what it got. For a plain object React lists its keys: object with keys {id, name}. For anything else it prints the type from Object.prototype.toString, such as [object Date] or [object Map]. The keys are usually enough to recognise the object in your code.
Production builds keep that detail. The minified message links to react.dev/errors/31 and passes the found value in the args[] part of the URL, so you can decode it from a production report.
Common causes
- An object rendered where a field was meant.
{user}instead of{user.name}, or{product.price}whenpriceis{ amount, currency }. - An error object rendered as text.
{error}from afetchwrapper, an API client or a form library, where the text is inerror.message. - A
Daterendered directly. The message saysfound: [object Date]. - Props not destructured.
function Title(title)receives the whole props object, so{title}renders{ title: '…' }and the message lists the keytitle. - An object passed to a component that renders its
children.<Badge>{status}</Badge>wherestatusis an object.
How to fix it
Render a field of the object
// Before: found: object with keys {id, name, email}
<p>Signed in as {user}</p>
// After
<p>Signed in as {user.name}</p>To see an object's contents while debugging, turn it into a string:
<pre>{JSON.stringify(user, null, 2)}</pre>Render the message of an error
// Before
{error && <p role="alert">{error}</p>}
// After: works for Error objects and plain { message } objects
{error && <p role="alert">{error instanceof Error ? error.message : String(error.message ?? error)}</p>}With React Hook Form, field errors are objects too. Render errors.email?.message.
Format dates as strings
// Before: found: [object Date]
<time>{order.createdAt}</time>
// After
<time dateTime={order.createdAt.toISOString()}>
{order.createdAt.toLocaleDateString('en-GB')}
</time>If the date is formatted during server rendering, format it the same way on both sides, or you can get a hydration mismatch instead. See Next.js hydration mismatch.
Destructure props
// Before: title is the whole props object
function Title(title) {
return <h1>{title}</h1>;
}
// After
function Title({ title }: { title: string }) {
return <h1>{title}</h1>;
}Map arrays of objects to elements
An array is a valid child only when its items are valid children. Map each object to an element with a key.
// Before: an array of objects
<ul>{orders}</ul>
// After
<ul>
{orders.map((order) => (
<li key={order.id}>{order.number}</li>
))}
</ul>For a Map or a Set, convert it first with Array.from(map.values()).
How to catch it in production with BugTape
In production React throws Minified React error #31 with the found value in the link. The error is thrown during render, so where it goes depends on your error boundaries.
- No error boundary of yours catches it. React 19 passes the error to
reportError(), which fires a windowerrorevent. React 18 rethrows it as an uncaught error. The BugTape browser SDK reports it automatically with the page URL, the stack and the clicks and requests before it. That last part often shows which API response had the unexpected shape. In the Next.js App Router, an error that only the built-in boundary catches also goes toreportError(). - Your error boundary catches it, including a Next.js
error.tsx. React and Next.js then only callconsole.error. BugTape records console output into a report but does not start one from it. Add one line to the boundary:
'use client';
import { useEffect } from 'react';
export default function Error({ error, retry }: { error: unknown; retry: () => void }) {
useEffect(() => {
reportError(error); // fires the window error event BugTape listens for
}, [error]);
return <button onClick={() => retry()}>Try again</button>;
}For a plain React 19 app, createRoot(el, { onCaughtError: (error) => reportError(error) }) does the same for every boundary.
The SDK records nothing before init() runs. When the error happens while a Server Component or server render runs in Next.js, the Node SDK's onRequestError hook reports it on the Node.js runtime.
Add the browser SDK once (see the quickstart):
import { init } from '@bugtape/sdk';
init({ apiKey: 'YOUR_PROJECT_KEY', release: 'YOUR_GIT_SHA' });Reports with the same error from the same place group into one issue with a count, so an API change that breaks one component for every user shows up as one issue. Set release so that when you mark the issue fixed, a report from a newer release reopens it as a regression. Your coding agent can read the issue over MCP (npx -y bugtape connect claude, or cursor or codex) and propose the change. You review the pull request.
FAQ
What is React error 31?
It is the production code for Objects are not valid as a React child (found: …). If you meant to render a collection of children, use an array instead. The link in the production message includes the found value, URL-encoded, after args[]=.
Why does it say "found: [object Promise]"?
That message comes from React 18 and earlier, which could not render a promise. The usual cause was an async function used as a component, or a missing await. React 19 accepts a promise as a child and suspends until it resolves. An async component marked 'use client' is still not supported, and React prints a development error saying it is an async Client Component.
Can I render an object if I convert it to JSON?
Yes. JSON.stringify(value) returns a string, which React renders. It is useful for debugging. For real UI, render the fields you need.
Why does the error only happen for some users?
The value comes from data, so it depends on what the API returned for that user. A field that is usually a string can be an object for some records, such as a name stored as { first, last }. The found keys in the message show which shape arrived.
Sources
- React error codes (31), checked 2 Nov 2026.
- React source, ReactChildFiber.js (
throwOnInvalidObjectType, promise children), checked 2 Nov 2026. - React source, formatProdErrorMessage.js (production message and
args[]), checked 2 Nov 2026. - React source, ReactFiberHooks.js (async Client Component warning), checked 2 Nov 2026.
- React docs: createRoot
onCaughtError, checked 2 Nov 2026.
Related errors
- Cannot read properties of undefined:
TypeError: Cannot read properties of undefined (reading 'map') - React: Too many re-renders:
Too many re-renders. React limits the number of renders to prevent an infinite loop. - Next.js hydration mismatch:
Hydration failed because the server rendered HTML didn't match the client - Next.js Server Components render error:
An error occurred in the Server Components render. The specific message is omitted in production builds - React: Maximum update depth exceeded:
Maximum update depth exceeded
All guides: Fix an error.
Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.