Home

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.

FactDetail
ErrorObjects 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 buildsMinified React error #31 with the found value in the link, such as args[]=object%20with%20keys%20%7Bid%2C%20name%7D
Where you see itBrowser 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 fixRender a string or number from the object, or map arrays to elements
BugTape catches itYes, 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

  1. An object rendered where a field was meant. {user} instead of {user.name}, or {product.price} when price is { amount, currency }.
  2. An error object rendered as text. {error} from a fetch wrapper, an API client or a form library, where the text is in error.message.
  3. A Date rendered directly. The message says found: [object Date].
  4. Props not destructured. function Title(title) receives the whole props object, so {title} renders { title: '…' } and the message lists the key title.
  5. An object passed to a component that renders its children. <Badge>{status}</Badge> where status is 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 window error event. 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 to reportError().
  • Your error boundary catches it, including a Next.js error.tsx. React and Next.js then only call console.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

All guides: Fix an error.

Start free. Free plan · No credit card · Set up in minutes. Or try the live demo first.