SSE events, partial JSON, and the failure modes buffering hides
Streaming changes the shape of every error path. A response that arrives in pieces can fail halfway, and the client has to decide what a half-answer means.
| Event | Carries | Handle by |
|---|---|---|
message_start |
Metadata and the input token count | Opening your accumulator |
content_block_start |
The type of the block beginning | Branching on text vs tool_use |
content_block_delta |
An incremental fragment | Appending — never replacing |
content_block_stop |
End of one block | Finalising that block |
message_delta |
stop_reason and output usage | Recording why it ended |
message_stop |
End of the response | Closing the connection |
ping |
Nothing | Ignoring — it only keeps the socket warm |
error |
A mid-stream failure | Surfacing it; the text so far may be incomplete |
A delta is a fragment, sometimes a single character. Append; never treat one as the whole value.
let text = '';
let stalled;
const resetStall = () => {
clearTimeout(stalled);
// A stream that simply stops emitting never "fails" on its own
stalled = setTimeout(() => controller.abort(), 30_000);
};
for await (const event of stream) {
resetStall();
switch (event.type) {
case 'content_block_delta':
text += event.delta.text ?? ''; // APPEND, never assign
break;
case 'content_block_stop':
render(text); // flush here, not per delta
break;
case 'error':
// Text may already be on screen — say it is incomplete
showPartialWarning(text, event.error);
break;
}
}
clearTimeout(stalled);
Not safely. Partial JSON is not JSON. Wait for the block to stop, or use a tolerant parser purely to render a preview and re-parse strictly at the end.
Say so. The user is already looking at half an answer, and silently leaving it there is the worst option. Mark it incomplete and offer to retry.