jsonlkit.com
JSONL (JSON Lines) utilities, in the browser
Say hi →

JSONL in JavaScript

Language guide · updated 28 July 2026 · overview · spec · format comparison · examples · best practices · troubleshooting · FAQ · glossary

JavaScript has no built-in JSONL parser, and the naive text.split("\n").map(JSON.parse) works right up until the file is bigger than memory. This page covers Node's readline streaming, async iterators, gzip, and the browser Streams API that lets a web page read a multi-gigabyte file without loading it.

Node.js: readline is the right primitive

readline handles the buffering and gives you one line at a time, so memory stays flat regardless of file size:

import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";

async function* readJsonl(path) {
  const rl = createInterface({
    input: createReadStream(path, "utf8"),
    crlfDelay: Infinity,          // treat \r\n as one break — Windows files
  });
  let lineno = 0;
  for await (const line of rl) {
    lineno++;
    if (!line.trim()) continue;
    try {
      yield JSON.parse(line);
    } catch (err) {
      throw new Error(`line ${lineno}: ${err.message}`);
    }
  }
}

for await (const record of readJsonl("data.jsonl")) {
  console.log(record.id);
}

crlfDelay: Infinity is not optional in practice — without it, a file with Windows line endings leaves a stray \r that JSON.parse tolerates at the end but which corrupts your last value if the line ends inside a string. Our encoding fixer normalises line endings if you would rather clean the file once.

Writing JSONL, and why you should await drain

Writing looks trivial and has one real trap: ignoring backpressure. If write() returns false and you keep writing, Node buffers the whole file in memory — exactly what streaming was meant to avoid:

import { createWriteStream } from "node:fs";
import { once } from "node:events";

async function writeJsonl(path, records) {
  const out = createWriteStream(path, "utf8");
  for (const record of records) {
    // JSON.stringify never emits a newline, so one record really is one line.
    if (!out.write(JSON.stringify(record) + "\n")) {
      await once(out, "drain");      // respect backpressure
    }
  }
  out.end();
  await once(out, "finish");
}

For a smaller file, joining in memory is fine and simpler: writeFileSync(path, records.map(r => JSON.stringify(r)).join("\n") + "\n"). Keep the trailing newline — without it, appending later joins two records on one line.

Streaming from a fetch response

A JSONL endpoint can be consumed as it arrives, which is the point of the format for streaming APIs — you process the first record before the last one is sent. The subtlety is that chunk boundaries do not respect line boundaries, so you must carry a remainder:

async function* streamJsonl(url) {
  const res = await fetch(url);
  const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;
    const lines = buffer.split("\n");
    buffer = lines.pop();           // last piece may be a partial line
    for (const line of lines) {
      if (line.trim()) yield JSON.parse(line);
    }
  }
  if (buffer.trim()) yield JSON.parse(buffer);
}

Forgetting buffer = lines.pop() is the classic bug: it works on small responses that arrive in one chunk and fails intermittently on large ones, which makes it miserable to debug.

In the browser: File + Streams, no upload

A browser can read a local file of any size with the same technique, because File is a Blob and Blob.stream() gives you a ReadableStream. This is exactly how the tools on this site handle 1 GB files without a server:

async function* readLocalJsonl(file) {          // file from <input type="file">
  const reader = file.stream().pipeThrough(new TextDecoderStream()).getReader();
  let buffer = "";
  for (;;) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += value;
    const lines = buffer.split("\n");
    buffer = lines.pop();
    for (const line of lines) if (line.trim()) yield JSON.parse(line);
  }
  if (buffer.trim()) yield JSON.parse(buffer);
}

Nothing is uploaded — the bytes never leave the tab. Combine it with DecompressionStream("gzip") and you can read a .jsonl.gz directly: file.stream().pipeThrough(new DecompressionStream("gzip")).pipeThrough(new TextDecoderStream()).

Gzip in Node

import { createReadStream, createWriteStream } from "node:fs";
import { createGunzip, createGzip } from "node:zlib";
import { createInterface } from "node:readline";
import { pipeline } from "node:stream/promises";

// Read .jsonl.gz line by line
const rl = createInterface({
  input: createReadStream("data.jsonl.gz").pipe(createGunzip()),
  crlfDelay: Infinity,
});
for await (const line of rl) {
  if (line.trim()) console.log(JSON.parse(line).id);
}

// Compress an existing file
await pipeline(
  createReadStream("data.jsonl"),
  createGzip({ level: 9 }),
  createWriteStream("data.jsonl.gz"),
);

Things that bite in JavaScript specifically

Frequently asked questions

Why not just split on newlines and map JSON.parse?

For a small file, do exactly that — it is clear and correct. It breaks in two situations: when the file is bigger than memory, and when the data arrives in chunks that do not align with line boundaries. Both are the reason the streaming versions above exist.

Which npm package should I use?

You rarely need one. ndjson and split2 are well-established if you want a stream transform to drop into a pipeline; both are thin wrappers over the buffering logic shown here.

Does JSON.parse handle a JSONL file directly?

No. It parses one document, so it fails at the start of line 2 with "Unexpected non-whitespace character after JSON". That error is the usual sign something is treating JSONL as JSON.

How do I write JSONL from a browser and let the user download it?

Build a Blob from the joined lines with type application/x-ndjson, then create an object URL and click a generated <a download>. Every tool on this site does exactly that.

Is NDJSON different from JSONL in JavaScript?

No — they are the same format under two names, and .ndjson and .jsonl are read identically. See NDJSON vs JSONL for the history of the two names.

Tools mentioned on this page

— S., [email protected]