mrkeyoor.com_
Thu 06 Aug 08:52 UTC
npmDataupdated 06 Aug 2026

csv-parse

csv-parse turns CSV text into arrays or objects. It is one package from the four-part CSV project at csv.js.org, alongside csv-stringify, csv-generate, and stream-transform. The core is a byte-level state machine wrapped in a Node stream.Transform, which means you can pipe a multi-gigabyte file through it and never hold more than one record in memory. On top of that sit two convenience shapes: a callback form that collects everything into an array, and a fully synchronous form exported from csv-parse/sync for when the input is already a string and small. It has been released since 2010, has no runtime dependencies, and its option list is long enough to handle almost every malformed export you will be handed: custom delimiters and quotes, comments, BOMs, ragged rows, value casting, byte offsets, and per-record hooks.

Verdict

The parser to reach for when the CSV comes from somewhere you do not control, because the option list covers the failures other parsers choke on and the streaming path holds up on files bigger than memory. Something lighter is a better fit for small, clean, known-good input.

API stability5/5The parse() signature and option names have been stable across majors for years, and recent majors were mostly packaging changes. The project even documents that 7.0.0 was published by accident with no breaking changes, which tells you how conservative the surface is
Docs5/5csv.js.org has a page per option with worked examples, plus dedicated sections for the info properties, the common error codes, and the different distributions. Very few packages this size document their error codes at all
Maintenance4/5Pushed 2026-08-05, releases every few weeks, a conventional-commits changelog per package, and prototype pollution fixes shipped promptly in 6.2.1 and 7.0.2. It is one maintainer at Adaltas with 45 open issues, which is the main risk
Ecosystem5/518M weekly downloads and part of the four-package CSV project that also covers stringifying, generating, and transforming, so the option vocabulary carries across the whole pipeline

Use it if

  • You are parsing files larger than memory and want real streaming with backpressure rather than a helper that buffers the whole thing and then hands you an array
  • Your input is not clean CSV: semicolon or tab delimiters, comment lines, a UTF-8 BOM, rows with the wrong column count, or quotes that only sometimes escape correctly. There is an option for each of those
  • You want zero dependencies in a data pipeline, which csv-parse has, at about 7.9 kB gzipped
  • You need per-record control: on_record to transform or drop rows as they stream, cast to convert values, info to get line numbers and byte offsets for error reporting, and on_skip to log bad rows instead of failing
Skip it if

Setup reality

npm install csv-parse is one package with no dependencies and no build step. The package publishes both ESM and CommonJS through the exports map, so import and require both resolve, and there are separate ./sync, ./stream, and ./browser/esm entry points. The trap is that /sync is a distinct import path, not an option, and people spend real time wondering why the default export returns a stream instead of rows. Version numbering deserves a note: the changelog records that 7.0.0 was published by mistake and contains no breaking changes over 6.x, so a major bump that reads as scary is not. Node's stream ergonomics are the other cost. In streaming mode you attach 'readable', 'error', and 'end' listeners yourself, and an unhandled 'error' event crashes the process, so wire the error listener before writing anything. If you would rather use for await, the parser is an async iterable and try/catch works normally. Finally, columns: true reads the first row as headers, which silently produces one fewer record than you expect if the file has no header line.

Patterns

Parse a string you already have in memorysync-parse-string

import { parse } from 'csv-parse/sync'

const records = parse(text, {
  columns: true,
  skip_empty_lines: true,
  trim: true,
})
// records: [{ id: '1', name: 'ada' }, ...]

csv-parse/sync is a separate entry point, not an option on the main one. It buffers everything and throws a CsvError on the first bad row, so keep it for input you know is small.

Stream a large file record by recordstream-a-file

import fs from 'node:fs'
import { parse } from 'csv-parse'

const parser = fs.createReadStream('events.csv').pipe(
  parse({ columns: true, skip_empty_lines: true }),
)

for await (const record of parser) {
  await handle(record)
}

The parser is an async iterable, so for await gives you backpressure and lets a normal try/catch see parse errors. Awaiting inside the loop pauses reading, which is exactly what you want when the consumer is slower than the disk.

Collect every record with a callbackcallback-collect-all

import { parse } from 'csv-parse'

parse(input, { columns: true }, (err, records, info) => {
  if (err) return done(err)
  console.log(records.length, 'records from', info.lines, 'lines')
})

The third callback argument carries the parse info: lines, records, bytes, invalid_field_length and more. This form holds the whole result in memory, so it is the convenience shape rather than the scalable one.

Handle a file that is not comma separatedcustom-delimiter-and-quotes

const records = parse(text, {
  delimiter: ';',
  quote: '"',
  escape: '\\',
  record_delimiter: ['\r\n', '\n'],
  bom: true,
})

// or let it work the delimiter out from a sample
const guessed = parse(text, { delimiter_auto: true, columns: true })

delimiter accepts an array when a file mixes separators. bom: true strips the UTF-8 byte order mark, and without it your first header key comes back with an invisible prefix and every lookup on it returns undefined.

Convert strings to numbers, booleans, and datescast-values

const records = parse(text, {
  columns: true,
  cast: (value, context) => {
    if (context.header) return value
    if (context.column === 'amount') return Number(value)
    if (context.column === 'active') return value === 'true'
    return value
  },
  cast_date: true,
})

cast runs for header cells too unless you check context.header, which is the usual cause of headers turning into NaN. context also carries index, lines, and quoting, so you can treat a quoted empty string differently from an unquoted one.

Drop or reshape records as they streamfilter-and-transform-rows

const parser = parse({
  columns: true,
  on_record: (record, { lines }) => {
    if (!record.email) return null
    return { line: lines, email: record.email.toLowerCase() }
  },
})

Returning null or undefined from on_record drops the record without emitting it, which keeps filtering out of your consumer loop. The hook runs once per record inside the parser, so anything slow in here stalls the stream.

Keep going when some rows are malformedtolerate-bad-rows

const parser = parse({
  columns: true,
  relax_column_count: true,
  relax_quotes: true,
  skip_records_with_error: true,
  on_skip: (err, raw) => {
    console.warn('skipped', err.code, raw)
  },
})

Without skip_records_with_error, the first ragged row ends the parse. relax_column_count_less and relax_column_count_more let you accept short rows but still reject long ones, which is usually what you actually want.

Read only part of a fileslice-input

// skip a two-line preamble, read 1000 data records
const records = parse(text, {
  from_line: 3,
  to: 1000,
  columns: true,
})

from_line and to_line count physical lines including the header, while from and to count records after parsing. Mixing them up on a file with quoted newlines gives you the wrong window, because one quoted field can span several lines.

Catch parse errors on the stream APIerror-handling-stream

import { parse, CsvError } from 'csv-parse'

const parser = parse({ columns: true })
parser.on('error', (err) => {
  if (err instanceof CsvError) {
    console.error(err.code, 'at line', err.lines)
  }
  response.destroy(err)
})
parser.on('readable', () => {
  let record
  while ((record = parser.read()) !== null) rows.push(record)
})

Attach the error listener before you write anything: an unhandled 'error' event on a stream takes the process down. CsvError carries a stable code such as CSV_RECORD_INCONSISTENT_FIELDS_LENGTH, which is what you should branch on rather than the message.

Type the records in TypeScripttyped-records

import { parse } from 'csv-parse/sync'

type Row = { id: string; email: string; amount: string }

const rows = parse(text, { columns: true }) as Array<Row>

// or with the generic on the streaming parser
// const parser = parse<Row>({ columns: true })

The generic is an assertion about the file, not a validation of it. A renamed column still type-checks and shows up as undefined at runtime, so validate the header row or run the records through a schema library.

Ignore comment lines and blank rowscomments-and-empty-lines

const records = parse(text, {
  columns: true,
  comment: '#',
  comment_no_infix: true,
  skip_empty_lines: true,
  skip_records_with_empty_values: true,
})

By default the comment character is honoured anywhere in a line, so a value containing a hash gets truncated. comment_no_infix: true restricts it to the start of a line, which is almost always the behaviour you meant.

Get line numbers and byte offsets per recordrecord-info-offsets

const parser = parse({ columns: true, info: true })

for await (const { record, info } of parser) {
  if (!isValid(record)) {
    console.error(`bad row at line ${info.lines}, byte ${info.bytes}`)
  }
}

info: true changes the emitted shape from the record to a { record, info } wrapper, so every downstream consumer has to be updated at the same time. The offsets are what let you point a user at the exact line of a 4 GB upload.

Alternatives

PackageRegistryPick it when
papaparsenpmYou need the same parser in the browser and in Node, want worker-thread parsing, or want auto-detection of the delimiter with a friendlier API
fast-csvnpmYou want parsing and formatting from one package with a stream API and first-party TypeScript types
csv-parsernpmYou just want a stream of objects from a well-formed file as fast as possible and none of the recovery options matter
csv-stringifynpmYou are writing CSV rather than reading it, and want the same option vocabulary as csv-parse from the same project