skip to content

In the graphql-ws subprotocol, how do next, error and complete messages relate to one subscription id?

level: middleimportance: should knowfreq 54%

answer

  1. One socket, many labelled streams
  2. The client picks the label, not the server
  3. One message repeats; two end things
  4. An execution result versus a bare error array
  5. A thrown resolver does not end the stream

basics

~20 s

Every stream carries a client-chosen id. next delivers one execution result and may repeat indefinitely; error delivers an array of errors and ends the stream; complete ends it normally and travels in both directions. After either terminator, nothing more is sent under that id.

solid answer

~40 s

One socket multiplexes many streams, so each is labelled by an `id` the **client** picks on its `subscribe` message; every later message about that stream repeats it. `next` carries a full execution result — the same `data`/`errors` shape a query returns — and one `next` is one event, repeated as long as the source produces. `error` carries an **array of errors** and is terminating; it is used for failures that stopped the operation running at all, such as validation failing. `complete` ends a stream, from the server when the source is exhausted and from the client as an unsubscribe. The distinction that matters: a resolver that throws while producing an event is a field error inside that `next` payload's `errors`, and the stream keeps going. Reusing a live id closes the socket with 4409.

code

graphql · 8 lines
graphql
subscription AdverseEvents($trialId: ID!) {
  adverseEventReported(trialId: $trialId) {
    eventId
    reportedAt
    severity
    site { siteCode }
  }
}

go deeper

for a junior

Recall the four message types and that the id ties them together: subscribe starts a stream, next delivers events, and either complete or error ends it. Know the client picks the id.

for a middle

Explain the payload shapes — next carries a full execution result, error carries an array of errors — and be able to say which failures land in which. The field-error-inside-next case is the one interviewers probe.

for a senior

Demonstrate the client-side consequences: tolerating a next that arrives after your own complete, never tearing down a feed because one event carried errors, and knowing that a duplicate id kills the whole socket rather than one stream.

for a principal

Own what the framing does not give you. The subprotocol has no acknowledgement, no sequence number and no replay, so a stream's ordering guarantee is only 'the order the server sent'. Decide deliberately what your clients do about a gap.

## One socket, many streams A single WebSocket carries every subscription a client has open, so every message about a particular operation needs a label. That label is the `id` field: a string the **client** chooses and puts on its `subscribe` message. The server never invents one. Every later message about that operation — from either side — repeats the same id, and messages that concern the connection rather than a stream carry no id at all. Ids only need to be unique among the streams currently live on that socket. Reusing an id that is still active is a protocol violation: the server closes the socket with **4409**, whose reason string names the duplicated id. Client libraries typically use a monotonically increasing counter per socket, which is fine. ## The four message types of a stream **`subscribe`** (client to server) carries the id and a `payload` object holding the operation: `query` (the document text), and optionally `variables`, `operationName` and `extensions`. Note the field is named `query` even when the document contains a subscription — it is the document, not the operation type. **`next`** (server to client) carries the id and a `payload` that is a full **execution result** — the same `{"data": …, "errors": …}` shape a query over HTTP would return. One `next` is one event. A live subscription emits as many as the source produces, with no bound and no ordering guarantee beyond the order the server sent them. **`error`** (server to client) carries the id and a `payload` that is an **array of errors**, not an execution result. It is terminating: after it, nothing more is sent under that id. **`complete`** carries only the id and travels in both directions. From the server it means the stream ended normally — the source was exhausted, or the server chose to end it. From the client it means *stop this stream*: an unsubscribe. After a client `complete`, the server must send no further `next` for that id, though a `next` already in flight may still arrive and a careful client tolerates that. ## The distinction that decides the interview The question that separates a candidate who has read the protocol from one who has watched a socket in a devtools panel is: **which failures ride inside `next`, and which produce an `error` message?** * An **`error` message** is for failures that prevented the operation from running at all — the document did not parse, it failed validation against the schema, a variable could not be coerced. Nothing executed, so there is no result to put in an execution result, only errors. The stream is over before it started. * A **field error** — a resolver threw while producing one event — is part of that event's execution result. It arrives inside a `next` payload's `errors` entry, alongside whatever `data` survived. **The stream does not end.** The next event may be perfectly fine. A concrete case from a clinical-trial registry graph makes this stick. A deploy tightened `AdverseEvent.severity` from a nullable field to a non-null one. The source system, meanwhile, still emits the occasional record with no severity graded yet. Before the deploy, those events arrived as `next` messages with `severity: null` and the dashboard rendered a dash. After it, the resolver's null is no longer legal for the declared type, the result for that event collapses and an entry appears in `errors` — but it is still a `next` message, and events 2, 3 and 4 keep arriving normally. The dashboard's own bug was that it treated *any* `errors` entry as the end of the feed and tore down the subscription, so a partial-data event looked like a dead stream. Reading the message `type` rather than the presence of `errors` is the fix. ## Single-result operations over the same channel A `subscribe` message is not restricted to subscription operations. A query or a mutation may be sent the same way, and the protocol handles it naturally: exactly one `next` with the result, then `complete`. That is how some clients route *all* their traffic over one socket rather than mixing HTTP and WebSocket. It also explains the message names — `next` and `complete` are stream vocabulary, and a single result is just a stream of length one. ## Terminal states, and the rule they enforce Three ways a stream ends: server `complete`, server `error`, client `complete`. All three are final for that id. The server must not send messages under a completed id, and the client must not send `complete` for a stream the server has already ended with `error` or `complete`. Once an id has been retired the client is free to reuse it, but almost nobody does. None of this is in the GraphQL specification, which stops at the execution result. The framing of results as `next`/`error`/`complete` under a client-chosen id is the subprotocol's contribution.

  • A resolver throws while producing one event. Does the client see an error message or a next message?
    A `next` message. The failure happened during execution of that event, so it is a field error and belongs in that event's execution result: the payload carries whatever `data` survived plus an `errors` entry. The stream is untouched and the following event may be perfectly healthy. An `error` message is reserved for failures that prevented the operation from running at all — a parse failure, a validation failure, a variable that could not be coerced — where there is no result to deliver.
  • What does a complete message mean when the client sends it rather than the server?
    It is an unsubscribe: the client is telling the server to stop that stream and release whatever backs it. The server must send no further `next` under that id, though a message already in flight may still arrive and a careful client tolerates one. From the server, the same message type means the stream ended on its own — the source was exhausted, or the server chose to end it.
  • Can a subscribe message carry a query or a mutation rather than a subscription?
    Yes, and the protocol handles it without a special case: exactly one `next` with the result, then `complete`. That is how a client can route all of its traffic over a single socket instead of splitting between HTTP and WebSocket. It also explains the message names — `next` and `complete` are stream vocabulary, and a single result is simply a stream of length one.
  • What happens if a client reuses an id that is still active?
    The server closes the whole socket with code 4409, and the reason string names the duplicated id. Ids only have to be unique among the streams currently live on that connection, so a per-socket counter is enough; an id that has been terminated by `complete` or `error` may legally be reused, though almost no client does. The severity of the response — killing the connection rather than rejecting the message — reflects that a duplicate id means the client has lost track of its own state.

saying these in an interview costs you the question

  • Thinks the server assigns the subscription id
  • Treats any errors entry as the end of the stream
  • Says next carries a bare array of errors
  • Believes error and complete can both arrive for one id
  • Assumes subscribe only accepts subscription operations
  • Expects the server to answer a client complete

context