Skip to content

How to parse a CSV file in JavaScript

Splitting each line on commas works until the first value contains a comma, a quote or a line break. A real CSV parser handles quoted fields, escaped quotes (""), CRLF line endings and multi-line values. This guide shows how to parse CSV in the browser and in Node.js with TabularJS, how to turn rows into objects using the header row, and how to deal with the files that break naive parsers: semicolon-separated exports, tab-separated files and non-UTF-8 encodings.

Terminal window
npm install tabularjs
import tabularjs from 'tabularjs';

Or, without a bundler:

<script src="https://cdn.jsdelivr.net/npm/tabularjs/dist/index.min.js"></script>

Given people.csv:

name,email,age,city
Ada Lovelace,[email protected],36,London
"Hopper, Grace",[email protected],85,"New York"
Alan Turing,[email protected],,Wilmslow

Pass the path:

import tabularjs from 'tabularjs';
const result = await tabularjs('./data/people.csv');
console.log(result.worksheets[0].data);
[
[ 'name', 'email', 'age', 'city' ],
[ 'Ada Lovelace', '[email protected]', '36', 'London' ],
[ 'Hopper, Grace', '[email protected]', '85', 'New York' ],
[ 'Alan Turing', '[email protected]', '', 'Wilmslow' ]
]

What to expect from CSV input:

  • A CSV file has one table, so result.worksheets has a single entry. It has no worksheetName.
  • data is an array of rows; each row is an array of strings. Every value is a string, including '36'. CSV has no types, so TabularJS does not guess them.
  • The header row stays in data[0]. It is not removed or used as keys.
  • Quoted fields are unquoted: "Hopper, Grace" becomes Hopper, Grace, "" becomes ", and a quoted value can span several lines.
  • Missing values are ''. A trailing newline at the end of the file does not add an empty row.

The worksheet also has columns, with title set to the column letter ('A', 'B'…). Text formats do not carry styles or formulas.

In the browser, pass the File from an input or a drop event. The call and the result are identical to Node.js.

<input type="file" id="csv" accept=".csv,.tsv,.txt" />
<script src="https://cdn.jsdelivr.net/npm/tabularjs/dist/index.min.js"></script>
<script>
document.getElementById('csv').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const result = await tabularjs(file);
console.table(result.worksheets[0].data);
} catch (err) {
console.error('Could not parse', file.name, err.message);
}
});
</script>

For a CSV on a server, fetch it and pass the bytes with extension: 'csv', so it is not mistaken for another text format:

const response = await fetch('/exports/people.csv');
const bytes = new Uint8Array(await response.arrayBuffer());
const result = await tabularjs(bytes, { extension: 'csv' });

To parse CSV text you already have in a string, encode it first. A string passed directly is treated as a file path (or as HTML when it starts with <):

const text = 'sku,qty\nA-1,4\nB-2,10\n';
const bytes = new TextEncoder().encode(text);
const result = await tabularjs(bytes, { extension: 'csv' });
console.log(result.worksheets[0].data);
// [ [ 'sku', 'qty' ], [ 'A-1', '4' ], [ 'B-2', '10' ] ]

Turn rows into objects using the header row

Section titled “Turn rows into objects using the header row”

Most code wants { name: 'Ada Lovelace', email: '...' } instead of positional arrays. Take the first row as keys and map the rest:

import tabularjs from 'tabularjs';
function csvToObjects(rows) {
const [header = [], ...body] = rows;
// TabularJS 2.2+ already strips the byte order mark; this keeps older versions safe
const keys = header.map((h, i) => h.replace(/^/, '').trim() || `column${i + 1}`);
return body
.filter((row) => row.some((v) => v.trim() !== ''))
.map((row) => Object.fromEntries(keys.map((key, i) => [key, row[i] ?? ''])));
}
const { worksheets } = await tabularjs('./data/people.csv');
const people = csvToObjects(worksheets[0].data);
console.log(people);
[
{ name: 'Ada Lovelace', email: '[email protected]', age: '36', city: 'London' },
{ name: 'Hopper, Grace', email: '[email protected]', age: '85', city: 'New York' },
{ name: 'Alan Turing', email: '[email protected]', age: '', city: 'Wilmslow' }
]

The helper handles three things that bite in practice:

  • Byte order mark. Excel’s “CSV UTF-8” export starts the file with an invisible . TabularJS 2.2 and later remove it for every kind of input. Older versions left it glued to the first header ('name') when you passed a Node.js file path, and then row.name is undefined even though the console shows name. Stripping it again costs nothing.
  • Blank headers. A column without a header gets column3, column4… instead of an empty-string key.
  • Blank lines. A blank line in the middle of the file is returned as a row of '' values. The filter drops it.

If two columns share the same header, the later one wins in Object.fromEntries. Rename duplicates before mapping if your files can have them.

Since all values are strings, convert the fields you need explicitly. Doing it per field is clearer than guessing types for the whole file, and it avoids classic accidents such as zip code '02139' becoming 2139.

const people = csvToObjects(worksheets[0].data).map((p) => ({
name: p.name,
email: p.email.toLowerCase(),
age: p.age === '' ? null : Number(p.age),
city: p.city,
}));
[
{ name: 'Ada Lovelace', email: '[email protected]', age: 36, city: 'London' },
{ name: 'Hopper, Grace', email: '[email protected]', age: 85, city: 'New York' },
{ name: 'Alan Turing', email: '[email protected]', age: null, city: 'Wilmslow' }
]

Watch for Number(''), which is 0, not NaN. Check for empty strings first, as above. For dates, prefer ISO strings (2024-03-15) in your exports; new Date('03/04/2024') is ambiguous between March and April.

Semicolon-separated files and the delimiter option

Section titled “Semicolon-separated files and the delimiter option”

The default delimiter for .csv is a comma. In many European locales, Excel saves “CSV” with semicolons because the comma is the decimal separator. Parsed with the default, such a file comes out wrong:

name;amount;city
José;1.234,50;München
Zoë;99,00;Zürich
const wrong = await tabularjs('./data/euro.csv');
console.log(wrong.worksheets[0].data[1]);
// [ 'Jos�;1.234', '50;M�nchen' ]

Two problems: the columns are split at the decimal commas, and the accented letters are broken. The second one is an encoding issue. Fix both with options:

const result = await tabularjs('./data/euro.csv', {
delimiter: ';',
encoding: 'windows-1252',
});
console.log(result.worksheets[0].data);
// [
// [ 'name', 'amount', 'city' ],
// [ 'José', '1.234,50', 'München' ],
// [ 'Zoë', '99,00', 'Zürich' ]
// ]
// "1.234,50" -> 1234.5
const parseEuro = (s) => Number(s.replace(/\./g, '').replace(',', '.'));
console.log(result.worksheets[0].data.slice(1).map((row) => parseEuro(row[1])));
// [ 1234.5, 99 ]

TabularJS does not guess the delimiter. If users upload files from different locales, count the candidates in the first line and pick the most frequent:

import { readFile } from 'node:fs/promises';
import tabularjs from 'tabularjs';
function guessDelimiter(sample) {
const firstLine = sample.split(/\r?\n/)[0];
const candidates = [',', ';', '\t', '|'];
const counts = candidates.map((d) => firstLine.split(d).length - 1);
const best = counts.indexOf(Math.max(...counts));
return counts[best] > 0 ? candidates[best] : ',';
}
const bytes = await readFile('./data/euro.csv');
const sample = new TextDecoder('windows-1252').decode(bytes.subarray(0, 4096));
const delimiter = guessDelimiter(sample); // ';'
const result = await tabularjs(bytes, { extension: 'csv', delimiter, encoding: 'windows-1252' });

In the browser, read the sample with await file.slice(0, 4096).text() and pass file itself to tabularjs. Counting on the header line is a heuristic: it is reliable for typical exports, but let users override it when it guesses wrong.

TabularJS decodes CSV, TSV and TXT as UTF-8 by default. Files exported by older Windows software, or by Excel’s plain “CSV (Comma delimited)” option on Windows, are usually in Windows-1252. Read as UTF-8, every accented character becomes �, as in the example above.

Set encoding to any label that TextDecoder understands:

Source encoding
Excel / Windows, Western Europe and Americas 'windows-1252' (or 'latin1', same decoder)
Windows, Central Europe 'windows-1250'
Windows, Cyrillic 'windows-1251'
Japanese 'shift_jis'
Chinese (Simplified) 'gbk'

In Node.js, old DOS codepages such as 'cp437' and 'cp850' also work, through the optional iconv-lite dependency. See the encoding option.

There is no reliable way to detect an 8-bit encoding from the bytes alone. A practical approach for uploads: parse as UTF-8 first, and if any value contains � (the � replacement character), parse again with windows-1252 or let the user pick the encoding.

Tab-separated files use the same parser with a tab delimiter. .tsv and .tab files always use tabs. .txt files default to tabs and accept a delimiter for anything else:

const tsv = await tabularjs('./data/scores.tsv'); // tab by default
const txt = await tabularjs('./data/pipe.txt', { delimiter: '|' });
console.log(tsv.worksheets[0].data);
// [ [ 'id', 'name', 'score' ], [ '1', 'Ada', '9.5' ], [ '2', 'Grace', '8' ] ]
console.log(txt.worksheets[0].data);
// [ [ 'id', 'name' ], [ '1', 'Ada' ], [ '2', 'Grace' ] ]

A .txt file that is actually comma-separated needs { delimiter: ',' }, or { extension: 'csv' }. The csvToObjects helper above works unchanged for all of them.

Columns shifted by one. Almost always a delimiter mismatch: a semicolon file read with commas, or a comma inside an unquoted value written by a broken exporter. Check data[0].length against the number of columns you expect and fail early.

Garbled accents. The file is not UTF-8. Set encoding.

Header key not found. A BOM or trailing spaces in the header. Normalize keys with .replace(/^/, '').trim().

Leading zeros and long numbers. IDs like '00451' or 16-digit card numbers are safe as strings. Converting every numeric-looking value to a number would corrupt them.

Formula injection on export. If you write user-supplied CSV values back out to a file someone opens in Excel, values that start with =, +, - or @ can run as formulas. TabularJS never evaluates anything when reading, but escape such values when you generate CSV.

Huge files. TabularJS parses the whole file in memory. That is fast for typical files (200,000 rows parse in a fraction of a second in Node.js), but for multi-gigabyte logs, a streaming parser such as Papa Parse or csv-parse that emits rows one at a time is the better tool.

Mixed uploads. If the same import accepts CSV and Excel, one tabularjs(file) call reads both. Remember that XLSX values are typed (numbers, booleans) while CSV values are strings, and normalize before comparing.