mrkeyoor.com_
Thu 06 Aug 09:54 UTC
npmDataupdated 06 Aug 2026

papaparse

Papa Parse turns CSV text into JavaScript arrays or objects and back again. You hand it a string, a File from an input element, a URL to download, or a Node readable stream, and it gives you rows plus a metadata object describing what it found. The parser follows RFC 4180, so quoted fields containing commas and embedded newlines come out correct, which is where most hand-rolled split(',') code falls apart. It has zero dependencies, weighs under 7 KB gzipped, can auto-detect the delimiter, can run in a Web Worker so a 200 MB file does not freeze the tab, and can stream row by row instead of building one enormous array in memory. Papa.unparse goes the other direction and writes CSV from your objects.

Verdict

For CSV in a browser, Papa Parse is still the right answer: correct on quoting, tiny, dependency-free, and the only option with worker and streaming support that just works. On the server it is merely adequate, and csv-parse or fast-csv fit Node's stream model better.

API stability5/5The v5 config object has been stable since 2018; new options get added, existing option names and callback signatures do not change, and code written years ago still runs on 5.5.4.
Docs4/5papaparse.com/docs is a complete single-page reference for every config key and result field, with a live demo you can throw a real file at. The weak spot is Node: its limitations are a couple of README paragraphs rather than a documented API of their own.
Maintenance3/55.5.4 shipped in June 2026 and the repository was pushed in July 2026, so it is not abandoned, but 190 issues are open (225 counting PRs) and plenty of them are years old with no maintainer reply.
Ecosystem5/5Roughly 14.3 million weekly downloads, zero dependencies, community TypeScript definitions in @types/papaparse, and it is the parser behind a large share of CSV import features in web apps.

Use it if

  • You accept CSV uploads in the browser and need to parse them client-side, with a progress indicator and no server round trip; this is the case Papa Parse was built for and nothing else does it as well
  • Your input is real-world messy CSV: quoted fields with commas and newlines inside them, unknown delimiter (comma, semicolon, tab, pipe), a BOM at the start, inconsistent line endings
  • The files are big enough that reading everything into memory is a problem, and you want a step or chunk callback that processes rows as they arrive, with pause, resume, and abort
  • You also need to generate CSV for download, and would rather use Papa.unparse than write your own quoting and escaping rules
Skip it if

Setup reality

npm install papaparse is genuinely painless: no dependencies, no peer dependencies, no build step, and it works from a script tag or a bundler. Two things surprise people. First, types are not included, so TypeScript users need npm install --save-dev @types/papaparse as a second step. Second, the package is browser-first: the default export targets the browser build, and the Node behaviors are documented as a list of options that quietly stop working rather than as a separate Node API. Worker mode also has a quirk that bites bundler users, because Papa builds the worker from its own script URL, and some bundler configurations make that URL unreachable, in which case parsing silently falls back or fails depending on setup. Everything else is config-object tuning against a single long options table.

Patterns

Parse a CSV string into objectsparse-string-with-header

import Papa from "papaparse";

const csv = "name,age\nAda,36\nGrace,45";
const result = Papa.parse(csv, {
  header: true,
  skipEmptyLines: true,
});

console.log(result.data);   // [{ name: "Ada", age: "36" }, ...]
console.log(result.errors); // []
console.log(result.meta.fields); // ["name", "age"]

Parsing a string is synchronous and returns the result directly. Values stay strings unless you turn on dynamicTyping. skipEmptyLines: 'greedy' also drops lines that contain only delimiters and whitespace.

Parse a file chosen in the browserparse-file-upload

const input = document.querySelector("input[type=file]");

input.addEventListener("change", () => {
  Papa.parse(input.files[0], {
    header: true,
    complete: (results) => {
      console.log(results.data.length, "rows");
    },
    error: (err) => console.error(err),
  });
});

Passing a File makes parsing asynchronous, so the return value is useless and you must use the complete callback. Add encoding: 'ISO-8859-1' when users upload files exported from older spreadsheet tools.

Fetch and parse a CSV by URLdownload-remote-csv

Papa.parse("https://example.com/data.csv", {
  download: true,
  header: true,
  downloadRequestHeaders: { Authorization: "Bearer " + token },
  complete: (results) => render(results.data),
});

download: true only works in the browser and is subject to CORS like any other request. In Node it is ignored, so fetch the body yourself and pass the string or stream.

Handle rows as they arrive instead of all at oncestream-row-by-row

Papa.parse(file, {
  header: true,
  step: (row, parser) => {
    if (!row.data.email) {
      parser.abort();
      return;
    }
    insertIntoDb(row.data);
  },
  complete: () => console.log("done"),
});

With step, results.data holds one row instead of the whole file, so memory stays flat. The parser argument gives you pause(), resume(), and abort(); pause and resume are what you need when your per-row work is asynchronous.

Process large files in batcheschunk-mode

Papa.LocalChunkSize = 1024 * 1024 * 5; // 5 MB per chunk

Papa.parse(file, {
  header: true,
  chunk: (results, parser) => {
    parser.pause();
    bulkInsert(results.data).then(() => parser.resume());
  },
  complete: () => console.log("all chunks handled"),
});

chunk gives you an array of rows per callback rather than one row, which is much faster than step for bulk inserts. Pause before an async call or Papa keeps feeding you chunks while your writes queue up.

Parse off the main threadweb-worker

Papa.parse(file, {
  worker: true,
  header: true,
  step: (row) => count++,
  complete: () => console.log(count, "rows parsed without freezing the UI"),
});

Worker mode is browser only and costs a serialization hop per row, so it is slower in total but keeps the page responsive. Functions in your config cannot cross into the worker, which rules out transform and beforeFirstChunk here.

Pipe a Node readable stream through Papanode-stream-pipe

import fs from "node:fs";
import Papa from "papaparse";

const parseStream = Papa.parse(Papa.NODE_STREAM_INPUT, { header: true });

fs.createReadStream("big.csv")
  .pipe(parseStream)
  .on("data", (row) => console.log(row))
  .on("end", () => console.log("finished"));

In NODE_STREAM_INPUT mode the step and complete callbacks are unavailable; you listen for 'data' and 'end' events instead. worker, download, and the chunk-size settings do nothing in Node either.

Write CSV from objectsunparse-to-csv

const rows = [
  { name: "Ada", note: 'said "hi", then left' },
  { name: "Grace", note: "multi\nline" },
];

const csv = Papa.unparse(rows, {
  columns: ["name", "note"],
  delimiter: ",",
  newline: "\r\n",
});

unparse quotes and escapes for you, which is the whole reason to use it. It builds the entire string in memory, so for very large exports write rows yourself in a loop or stream from the server.

Clean up headers and values while parsingtransform-headers-and-values

Papa.parse(csv, {
  header: true,
  transformHeader: (h) => h.trim().toLowerCase().replace(/\s+/g, "_"),
  transform: (value, field) => (field === "price" ? Number(value) : value.trim()),
});

Doing typed conversion in transform is safer than dynamicTyping because you decide field by field. transformHeader also receives the column index as a second argument, useful when duplicate header names need disambiguating.

Check what went wrong and what was detectedinspect-errors-and-meta

const result = Papa.parse(csv, { header: true });

for (const e of result.errors) {
  console.log(e.type, e.code, e.message, "row", e.row);
}

console.log(result.meta.delimiter);    // ","
console.log(result.meta.linebreak);    // "\n"
console.log(result.meta.truncated);    // true if preview cut it short
console.log(result.meta.aborted);

Papa almost never throws; it collects problems in errors and keeps going, so an empty data array with a populated errors array is the failure mode to check for. TooFewFields and TooManyFields are the two codes you will see most.

Control delimiter detection and peek at the first rowsdelimiter-and-preview

const peek = Papa.parse(csv, {
  preview: 5,
  delimitersToGuess: [",", ";", "\t", "|"],
});
console.log("detected:", peek.meta.delimiter);

// or skip guessing entirely
Papa.parse(csv, { delimiter: ";", quoteChar: '"', escapeChar: '"' });

Auto-detection samples the beginning of the file, so a file whose first rows happen to contain semicolons inside quoted commas can be guessed wrong. Setting delimiter explicitly is both faster and safer when you control the source.

Wrap parsing in a promisepromise-wrapper

function parseCsv(input, config = {}) {
  return new Promise((resolve, reject) => {
    Papa.parse(input, {
      ...config,
      complete: resolve,
      error: reject,
    });
  });
}

const { data, errors } = await parseCsv(file, { header: true });

There is no built-in promise API, so most codebases end up with a helper like this. Do not use it together with step, because step-based parsing resolves with an empty data array.

Alternatives

PackageRegistryPick it when
csv-parsenpmYou are in Node and want a real stream transform with backpressure, sync and async iterator APIs, and a large set of RFC-edge-case options.
fast-csvnpmNode again, but you want parsing and formatting under one friendly API with per-row validation and transform hooks.
d3-dsvnpmYou are parsing modest CSV or TSV in a browser data-visualization context and want a tiny, synchronous parser with a row conversion function.