In React 19, a Server Component passes an un-awaited promise as a prop to a Client Component instead of awaiting it first. What does React do with that promise, and what does the client side have to get right?
answer
- promise is a serializable prop type
- pending reference now, value later
- the stream is append-only
- resolved value obeys the same rules
- rejection still needs a boundary
basics
~20 sPromises are serializable across the RSC boundary. React sends the prop as a pending reference and streams the resolved value as a later chunk of the same response, so the server does not block. The resolved value must itself be serializable, and rejection needs an error boundary.
solid answer
~50 sA promise is one of the serializable prop types in React 19, so passing it un-awaited is deliberate, not a mistake. React emits the prop as a reference to a not-yet-complete chunk of the RSC stream, finishes rendering everything else, and writes the resolved value into a later chunk when it settles. The payoff is that a slow query no longer blocks the server render: the shell and all the fast content reach the browser first, and the slow data fills in. Two things must be right on the client. The resolved value obeys the same serialization rules as any prop, so resolving to a class instance or a function fails just as it would directly. And the client component that reads the promise suspends, so it needs a `<Suspense>` boundary above it for the pending state and an error boundary for rejection — an un-handled rejected promise crossing the boundary is a real error, not a silent no-op.
code
jsx · 14 lines// Comments.jsx — the client side of a promise prop
'use client';
import { use } from 'react';
export default function Comments({ commentsPromise }) {
const comments = use(commentsPromise);
return (
<ul>
{comments.map((c) => (
<li key={c.id}>{c.body}</li>
))}
</ul>
);
}go deeper
Recall that a promise is allowed as a prop from a Server Component, and that the client shows a fallback while it is pending. Knowing that the data still comes from the server, not a second browser request, is the point to land.
Explain the streaming mechanics: React writes a pending reference into the payload and appends the resolved value as a later chunk, so the server render never blocks on the slow data.
Show the judgment — defer only when the data is slow and something useful can render meanwhile — and name the two obligations: the resolved value must be serializable, and both a Suspense boundary and an error boundary must exist above the reader.
Own when this pattern is worth its complexity across a codebase: the fallback design and layout-shift cost, where projections into serializable shapes happen, and whether deferring is solving a real latency problem or merely relocating it.
## Why a promise is a legitimate prop The instinct from a plain async server component is to `await` before rendering. That is correct and simple, but it makes the render of that subtree — and everything after it — wait for the slowest data it touches. React 19 makes promises part of the serializable set precisely so you can decline that. When a promise is passed as a prop, React writes a reference into the payload for a chunk that has not arrived yet, and carries on. The rest of the tree serializes immediately. When the promise settles, React writes the resolved value into a later chunk of the same response stream, and the client resolves its side of the promise then. Conceptually the payload is not a single document but an append-only stream with forward references, and a pending promise is one of those references. ## What it buys you The practical win is removing a waterfall without moving the fetch to the browser. The server starts the slow query, hands the promise down, and everything that does not depend on it renders and streams straight away. The user sees the page structure and the fast content while the slow part is still in flight, and the data still comes from the server — no extra round trip from the client, no client-side fetching code, no exposure of the data source. ```jsx // Server Component — start it, do not await it export default function Page() { const commentsPromise = db.comments.forPost(id); // no await return ( <article> <PostBody /> <Suspense fallback={<CommentsSkeleton />}> <Comments commentsPromise={commentsPromise} /> </Suspense> </article> ); } ``` ## What has to be right on the client **The resolved value is a prop too.** Serialization applies to whatever the promise settles with, under exactly the rules that apply to any other prop: plain objects, `Date`, `Map`, arrays and the rest are fine; a class instance from your ORM or a function is not. A promise is not an escape hatch around the boundary — it is a deferred crossing of the same boundary. Errors here appear late, when the chunk is written, which makes them harder to spot than the immediate throw you get from passing the same object directly. **Reading it suspends.** The client component consumes the promise with `use`, and while it is pending the component suspends, so there must be a `<Suspense>` boundary above it. Without one the suspension propagates upward until it finds a boundary — or fails to — and you lose the placement control that made this worth doing in the first place. **Rejection needs a handler.** If the promise rejects, the error surfaces where it is read, and you need an error boundary above that point to render something sensible. A rejected promise crossing the boundary with no consumer or no boundary is a genuine unhandled rejection, not a silently ignored value. ## The judgment call Await in the server component when the data is fast, when the component below cannot render anything meaningful without it, or when you want the simplest possible code — which is most of the time. Pass the promise when the data is slow relative to the rest of the page and there is something useful to show meanwhile. One more consideration: the shape of the fallback. Deferring the data means committing to a placeholder, and a placeholder whose size does not match the eventual content trades a slow render for a visible layout shift. That is a real cost to weigh, not a free optimisation. ## The trap to name Creating a fresh promise on the *client* during render is the mirror-image mistake: each render starts new work, and the component may never settle. That is not what this pattern is — here the promise is created once on the server, and the client only reads a value that is already on its way. Being able to draw that distinction is usually what the interviewer is checking. ## Saying it compactly "Promises serialize. React sends a pending reference and streams the value in a later chunk, so a slow query stops blocking the render. The resolved value still has to be serializable, the reader suspends so it needs a Suspense boundary, and rejection needs an error boundary."
- The promise resolves to an ORM model instance. What happens, and when do you find out?It fails on the same plain-object rule as passing the instance directly, but later — when the chunk is written rather than during the initial render. That delayed feedback is a reason to project into a plain view model where the promise is created, not where it is consumed.
- When would you await in the Server Component instead of passing the promise down?When the data is fast, when nothing meaningful can render without it, or when the deferred version would only trade a short wait for a layout shift. Awaiting is simpler and needs no fallback; passing the promise is worth the extra machinery only when the wait is genuinely long.
- How is this different from creating a promise inside a client component during render?Completely different. Here the promise is created once on the server and the client only reads a value already in flight. Creating one during a client render starts new work every render, so it may never settle — that is the anti-pattern this pattern is often confused with.
saying these in an interview costs you the question
- Says React awaits the promise before serializing it
- Thinks passing a promise skips the serialization rules
- Believes the client refetches the data itself
- Assumes rejection is handled silently by React
- Confuses it with creating promises during a client render