A network chunk is not a complete JSON message. When you stream AI output, one object may arrive in several pieces and several objects may arrive together.

What you will build

Write and test a newline-delimited JSON parser that handles JSON objects split across network chunks.

Before you start

Node.js 18 or later for the executable fixture. The parser also works with text chunks produced by a browser TextDecoderStream.

1. Create ndjson.cjs

function createNdjsonParser(onValue) {
  let pending = "";
  function parseLine(line) {
    if (line.trim()) onValue(JSON.parse(line));
  }
  return {
    push(chunk) {
      pending += chunk;
      if (pending.length > 1024 * 1024) throw new Error("Buffer limit exceeded");
      let end;
      while ((end = pending.indexOf("\n")) !== -1) {
        const line = pending.slice(0, end);
        pending = pending.slice(end + 1);
        parseLine(line);
      }
    },
    finish() {
      parseLine(pending);
      pending = "";
    }
  };
}
const tokens = [];
const parser = createNdjsonParser(value => {
  if (value.error) throw new Error(String(value.error));
  if (typeof value.response === "string") tokens.push(value.response);
});
parser.push('{"response":"Hel');
parser.push('lo"}\n{"response":" world"}\n');
parser.push('{"done":true}');
parser.finish();
if (tokens.join("") !== "Hello world") throw new Error("Incorrect output");
console.log(tokens.join(""));

2. Run the example

node ndjson.cjs

Expected result

Hello world is printed even though the first JSON object was split across two input chunks.

How it works

The parser keeps incomplete text between calls. It only invokes JSON.parse after locating a newline, then consumes a final non-empty line when finish runs. Blank lines are ignored.

This is an NDJSON parser, not a universal AI streaming parser. An SSE endpoint uses a different framing protocol, often with data: prefixes. Match the parser to the documented endpoint before connecting it to a real service.

For fetch integration, check response.ok before reading the body. Decode bytes with TextDecoderStream or a streaming TextDecoder so a multi-byte character split across byte chunks is preserved. Feed the decoded strings to push and call finish at end-of-stream.

The buffer ceiling keeps the small demo from accumulating an unlimited unterminated line. Production readers should also bound total output and support cancellation. Render generated content with textContent unless you apply a reviewed HTML sanitizer.

Test the edge cases

  • Feed one character at a time and compare the output.
  • Append a malformed JSON line and confirm the parser throws.
  • Test CRLF endings and a last line without a newline.

Keep learning

Browse related tutorials. Reference: official documentation.

Reviewed 7 November 2026. AI-dependent examples require your own local model; generated text varies.