# JSON.parse throws on every token your LLM streams. Here's a 425-byte fix.

> Source: <https://dev.to/acegikmo135/jsonparse-throws-on-every-token-your-llm-streams-heres-a-425-byte-fix-1cne>
> Published: 2026-09-18 16:26:24+00:00

I was building a small app where an LLM returns a recipe as JSON and the UI fills in as the answer streams. Simple idea. It fell over immediately.

Here's what my handler actually saw between tokens:

```
{"title": "Pad Th
{"title": "Pad Thai", "ingr
{"title": "Pad Thai", "ingredients": ["rice noo
```

`JSON.parse` throws on every one of those. Every single token, until the very last `}`.

So you have two bad options. Wait for the whole thing to finish — which throws away the reason you're streaming in the first place. Or write a regex that closes some brackets and hope. Mine broke on the first escaped quote.

I wanted a third option: on every chunk, give me the best value you can honestly get from what's arrived so far. Never throw. Never make things up.

That turned into a tiny library called **SoFar**. It's 425 bytes gzipped, has zero dependencies, and does exactly one thing.

``` js
import { parsePartialJSON } from "sofar-json";

parsePartialJSON('{"title": "Pad Th');
// → { title: "Pad Th" }

parsePartialJSON('{"title": "Pad Thai", "ingr');
// → { title: "Pad Thai" }

parsePartialJSON('{"title": "Pad Thai", "servings": 4,');
// → { title: "Pad Thai", servings: 4 }

parsePartialJSON('{"ok": tru');
// → {}

parsePartialJSON('');
// → undefined
```

Notice the fourth one. `tru` is *probably* going to be `true`, but the buffer doesn't say that yet, so you get the last value that was actually complete. A partial string comes back as a string, because a prefix of a string is still a string. A partial number or literal doesn't get guessed.

For the streaming case there's a small stateful wrapper:

``` js
import { createJSONStream } from "sofar-json";

const stream = createJSONStream();

for await (const chunk of llmResponse) {
  const value = stream.feed(chunk);
  if (value !== undefined) render(value);
}

JSON.parse(stream.raw); // strict parse once it's done
```

That's the whole API. Two functions.

I didn't want a second JSON parser. `JSON.parse` is fast, correct, and already in every runtime. I just needed to hand it something it could accept.

So the library scans the buffer once, left to right, keeping track of three things:

`{` and `[`
Each cut point stores a snapshot of the stack at that moment. Not a copy — the stack is kept as the string of closing brackets you'd need to append, innermost first, so a snapshot is just a reference. Rewinding later is a slice and a concat. No rescanning.

Then it tries two things:

**Attempt 1.** If we're inside a string, close it. Append the closers for every open container. `JSON.parse`. This handles almost everything: a string cut mid-word, an array mid-element, an object mid-value.

**Attempt 2.** If that fails — a dangling key like `{"a": 1, "b`, a trailing comma, a half-typed `fals` — walk the cut points backwards. Slice the buffer at each one, append that cut's stored closers, try again. The first one that parses wins.

If nothing parses, return `undefined`. Never throw.

There's a bit of extra care for escapes. A trailing `\` gets dropped, a half-finished `\u00e` gets trimmed back to the backslash, but `\\` at the end of a string is left alone because that's a complete escape. The scanner already knows which case it's in, so this costs nothing.

The whole thing is one pass, O(n), and on a 1.6 MB buffer it runs at about 1.3× the cost of a bare `JSON.parse`.

I checked the alternatives with the same inputs before writing this. Sizes are esbuild + gzip.

| Input | SoFar | partial-json | best-effort-json-parser | jsonrepair | 
|---|---|---|---|---|
| Size | 425 B | 1.6 kB | 1.9 kB | 3.7 kB | 
| `''` | `undefined` | throws | `""` | throws | 
| `}}}` | `undefined` | throws | returns `"}}}"` | throws | 
| `{"ok": tru` | `{}` | `{ok: true}` | `{ok: true}` | `{ok: "tru"}` | 
| `{"a":"x","ingr` | `{a: "x"}` | `{a: "x"}` | `{a: "x"}` | `{a: "x", ingr: null}` | 

They're all fine libraries and I'd used a couple of them. What I wanted was different: never throw, never invent, and be small enough that I'd never think about it in a bundle. If your JSON has broken *syntax* — single quotes, comments, unquoted keys — `jsonrepair` is the right tool, and the two chain nicely.

`4.` is `4`. That could be `4.5`.
There's a playground where you can paste any JSON and scrub through it a character at a time. It shows the exact string handed to `JSON.parse`, which characters were synthesized, which were rewound past, and every cut point the scanner recorded:

[https://acegikmo135.github.io/sofar/](https://acegikmo135.github.io/sofar/)

```
npm install sofar-json
```

Source, tests, and the fuzz test that feeds a document one character at a time are on GitHub: [https://github.com/acegikmo135/sofar](https://github.com/acegikmo135/sofar)

If you've got a stream it handles badly, the most useful thing you can send me is the raw buffer as a string. It becomes a test case.
