skip to content

When a server-rendered route hands values to the client, which kinds cross intact, which arrive as a different type, and which are rejected?

level: middleimportance: must knowfreq 58%

answer

  1. the format decides, not the language
  2. three outcomes: intact, degraded, rejected
  3. silent degradation is the dangerous one
  4. works on the server, breaks after hydration

basics

~20 s

Plain data crosses intact: strings, finite numbers, booleans, null, arrays, plain objects. Richer values either degrade to a plain equivalent (a date arrives as a string, a class instance as a bare object) or are rejected, as cyclic structures are.

solid answer

~50 s

The boundary is a serialization step, so the wire format decides what survives. With a plain JSON-shaped wire, only JSON's own vocabulary crosses intact: strings, finite numbers, booleans, `null`, arrays and plain objects. Everything richer either **degrades** - a date arrives as a string, a class instance as a bare object with its methods gone, a missing value as `null` - or is **rejected**, which is what happens to cyclic structures and arbitrary-precision integers. Functions are not data and do not cross as themselves, though some frameworks hand across a callable *reference* to a server-side function, which is a different mechanism. Several frameworks use an extended encoding that preserves dates, maps, sets, repeated references and even values still resolving, streamed in later. The practical rule is to normalise at the boundary: send the primitive you actually want and reconstruct on the client, rather than discovering the loss at runtime.

go deeper

for a junior

Recall the short list that always survives: strings, finite numbers, booleans, null, arrays and plain objects. Anything richer needs checking before you rely on it in the browser.

for a middle

Explain the three outcomes - intact, silently degraded, rejected - and give an example of each. Being able to name the degradation cases is the core of this question.

for a senior

Show the diagnosis: same code passing on the server and failing after hydration, a type that changed at the crossing, and a boundary normalisation that stops the whole class rather than one instance.

for a principal

Argue the contract. Depending on a framework's richer encoding buys ergonomics and quietly couples your data shapes to it; normalising at the boundary is the version that survives a migration.

## The boundary is a serialization step Nothing is really *passed* across the server/client seam. A value is **encoded into text**, put in the document, and **decoded into a new value** in the browser. The two values are related only by whatever the format could express. So the question is never really what the framework allows - it is what the **wire format** can represent. Most meta-frameworks start from JSON, whose entire vocabulary is: string, number, boolean, `null`, array, object. Some build a richer encoding on top so that more of the language survives. Knowing which one you are on is what makes this predictable. ## Three outcomes, not two A value meets one of three fates, and conflating the first two is the usual source of bugs: 1. **Crosses intact** - the decoded value is equivalent to the original. 2. **Degrades quietly** - something arrives, but it is a different type. No error is raised, and the bug surfaces later, far from the boundary. 3. **Is rejected** - the encoding fails and you get an error at the crossing, which is by far the friendlier failure. ## What happens to each kind | Value | On a plain JSON-shaped wire | On a richer encoding | |---|---|---| | String, finite number, boolean, `null` | Intact | Intact | | Array, plain object | Intact | Intact | | Date | Degrades to a string; the type is gone | Usually preserved as a date | | Class instance | Degrades to a plain object; methods and prototype gone | Often still plain data unless a custom rule exists | | Keyed and unique-value collections | Degrade to an empty object - silent total data loss | Usually preserved | | Absent value | Property is dropped from an object; becomes `null` inside an array | Often distinguished from `null` | | Not-a-number and infinities | Become `null` | Often preserved | | Arbitrary-precision integer | Rejected - encoding throws | Usually preserved | | Repeated reference to one object | Duplicated, becoming two objects | Often preserved as one | | Cyclic structure | Rejected - encoding throws | Often preserved | | Function, symbol | Dropped from an object, or rejected outright | Not data either way | | Value still resolving | Not representable | Some encodings stream it and resolve it later | ## The large-number trap is upstream of the wire A 64-bit identifier is the classic example, and the usual explanation is wrong. The wire does not round it: the **number type** did that before serialization ever ran, the moment the value was held as a double beyond the safe-integer range. The payload then faithfully carries the already-damaged value, and the identifier that comes back differs from the one in the database by a digit or two. Sending such identifiers as **strings** end to end is the fix; noticing that arbitrary-precision integers are *rejected* rather than rounded is the tell that these are two different problems. ## The debugging signature This class of bug has a recognisable shape: - The **same code works on the server and fails in the browser** - a date method is missing, a collection lookup returns nothing, a comparison that used to sort correctly now sorts as text. - The failure lands **after hydration**, never during the server render. - Logging the value on both sides shows the same content and a different type. - A missing field turns out not to be missing at all - it simply had no representation and was dropped on the way across. The habit that prevents all of it: when a value crosses, ask what the format can express, not what the language can hold. ## Functions deserve their own note Functions are behaviour, not data, so a function does not cross. What some frameworks do offer is a **reference to a server-side function** that the client can invoke, which sends an identifier across and calls back to the server. That is a remote-call mechanism wearing a function's clothes; the code never leaves the server, and it is worth saying so in an interview rather than calling functions serializable. ## Designing for the crossing - Normalise at the boundary: hand over an instant as a number or an agreed string, an identifier as a string, a collection as an array of entries. - Reconstruct explicitly on the client, in one place, rather than scattering type-repair through components. - Do not build on a richer encoding you may not keep; if the code must survive a change of framework, the JSON vocabulary is the portable floor. - Test the crossing, not just the server value - a test that only runs server-side will never see the degradation.

  • Why is rejection at the boundary generally better news than silent degradation?
    Rejection fails at the crossing, with the offending value in hand, usually at build or render time. Degradation produces a plausible value that fails later in unrelated code - a sort that orders by text, a method that is suddenly missing - so the distance between cause and symptom is large and the type has already changed.
  • If a framework offers an encoding richer than JSON, is there any reason not to rely on it?
    Portability and clarity. Code that leans on preserved dates, collections and shared references silently depends on one framework's encoding, so any change of framework or of a boundary becomes a data-shape migration. Normalising to plain values costs a little boilerplate and keeps the contract explicit and inspectable.
  • A value arrives on the client as null although the server clearly had something. What are the likely explanations?
    It had no representation in the format and was replaced: a not-a-number result or an infinity, an absent value inside an array, or an entry that was dropped from an object entirely. It can also be an unrepresentable collection that encoded as an empty object, so the field exists but its contents vanished.

saying these in an interview costs you the question

  • Says anything can cross because it is all just JavaScript on both sides.
  • Believes a date crosses as a date on every framework.
  • Thinks the serialization step rounds large integers rather than the number type.
  • Assumes a class instance keeps its methods after the crossing.
  • Treats a dropped field as a framework bug rather than an unrepresentable value.
  • Claims functions are serialized and shipped to the client.