I sent a customer contact list to a marketing agency and they called me immediately. Every name with an accent was corrupted — “René” appeared as “René”, “José” as “José”. The CSV exported perfectly from our database. Excel destroyed it silently on open. A single 3-byte fix at file generation time would have prevented the entire problem.
Convert or fix CSVs without uploading them.
Our CSV to JSON converter handles BOM-prefixed files correctly. Your data stays in your browser.
Open CSV to JSON Converter →Why Excel Breaks UTF-8 CSVs
Excel's CSV parser does not default to UTF-8. When you double-click a .csv file, Excel uses the local Windows codepage — typically Windows-1252 on Western systems. Windows-1252 is an 8-bit encoding that represents only 256 characters. It matches ASCII for the first 127 characters, but diverges for accented letters and symbols.
UTF-8 encodes accented characters as multi-byte sequences. The letter “é” is0xC3 0xA9in UTF-8 — two bytes. Windows-1252 reads those same two bytes as separate characters: “Ô (0xC3) and “©” (0xA9). That is where “René” becomes “René”.
The fix is to signal the encoding before Excel reads any data rows. Excel checks for a Byte Order Mark (BOM) at the very start of the file. If found, it reads the file as UTF-8. If not found, it falls back to the local codepage.
The UTF-8 BOM: Three Bytes That Change Everything
The UTF-8 BOM is defined by theUnicode Consortiumas the byte sequence 0xEF 0xBB 0xBF. These three bytes appear at the very beginning of the file — before the header row, before any data.
# File without BOM (breaks Excel) 4e,61,6d,65,2c,45,6d,61,69,6c → Name,Email 52,65,6e,c3,a9,2c,72,65,6e,65 → René (broken!) # File with BOM (Excel reads correctly) ef,bb,bf,4e,61,6d,65,2c,45,6d → [BOM]Name,Email 61,69,6c,0a,52,65,6e,c3,a9,2c → René (correct)
Adding a BOM in JavaScript — The Correct Way
The cleanest way to inject the BOM during a browser-side CSV download uses aUint8Array prepended to the file blob:
// Step 1: Build your CSV string as UTF-8 text const rows = [ ['Name', 'Email', 'City'], ['René', '[email protected]', 'Paris'], ['José', '[email protected]', 'Madrid'], ]; const csvContent = rows.map(row => row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(',') ).join('\n'); // Step 2: Prepend the UTF-8 BOM (0xEF 0xBB 0xBF) const bom = new Uint8Array([0xEF, 0xBB, 0xBF]); // Step 3: Combine BOM + CSV content into a Blob const blob = new Blob([bom, csvContent], { type: 'text/csv;charset=utf-8;' }); // Step 4: Trigger download const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = 'contacts-utf8.csv'; link.click(); // Step 5: Clean up object URL to avoid memory leak URL.revokeObjectURL(url);
Adding a BOM in Node.js
When writing CSV files on the server side using Node.js, prepend the BOM constant to the file write buffer:
import { writeFileSync } from 'fs';
const BOM = ''; // UTF-8 BOM as Unicode escape
const csvContent = 'Name,Email\nRené,[email protected]\nJosé,[email protected]';
// Prepend BOM before writing
writeFileSync('output.csv', BOM + csvContent, { encoding: 'utf8' });
// Verify: the resulting file starts with bytes EF BB BF
// You can check this with: xxd output.csv | head -1Common Encodings Compared
| Encoding | Bytes per Char | Coverage | Excel Default | Web Standard |
|---|---|---|---|---|
| ASCII | 1 | 128 characters | Partial | No |
| Windows-1252 | 1 | 256 characters (Western) | Yes (problematic) | No |
| UTF-16 | 2–4 | All Unicode | With BOM | Uncommon |
| UTF-8 with BOM ★ | 1–4 | All Unicode | Yes (correct) | Yes |
Edge Cases and Troubleshooting
Double BOM — Silent Data Corruption
If your CSV generation pipeline applies the BOM and then another layer (a proxy, middleware, or file concatenation script) also prepends a BOM, you get a double BOM:0xEF 0xBB 0xBF 0xEF 0xBB 0xBF.
Excel handles the first BOM and renders the second as the character “” in cell A1. Your header row becomes Name instead of Name. The invisible character then breaks any VLOOKUP or column matching against that header.
// Detect and strip existing BOM before prepending
function addBomSafely(csvString: string): string {
const BOM = '';
// Strip any existing BOM first
const stripped = csvString.startsWith(BOM)
? csvString.slice(1)
: csvString;
return BOM + stripped;
}Trailing Commas Break Row Parsing
Some CSV generators append a trailing comma after the last field on each row. RFC 4180 (the CSV standard) does not prohibit this, but many parsers interpret a trailing comma as an empty additional column.
# Trailing comma — creates an empty 4th column Name,Email,City, René,[email protected],Paris, ← extra empty cell # Correct — no trailing comma Name,Email,City René,[email protected],Paris
This manifests in Excel as an extra blank column (column D) that breaks column-count assumptions. Strip trailing commas during generation using a .trimEnd(',')operation on each row string, or filter them with a regex.
CRLF vs LF Line Endings
RFC 4180 defines CSV line endings as CRLF (\r\n — carriage return + line feed). But most modern tools generate LF-only (\n) line endings.
Excel on Windows handles both. Excel on macOS has historically been inconsistent. If a CSV file opens in Excel as a single long row with no line breaks visible, the file probably uses CRLF and your terminal/preview tool is just hiding the \r character.
// Force CRLF line endings for maximum Excel compatibility
const csvContent = rows
.map(row => row.join(','))
.join('
'); // RFC 4180 compliant — not just '
'Commas and Newlines Inside Cell Values
If a cell value contains a comma or newline, RFC 4180 requires wrapping the entire value in double quotes. If the value also contains double quotes, those must be escaped by doubling them.
// Safe cell escape function — handles commas, newlines, quotes
function escapeCell(value: string): string {
const needsQuoting = /[,"
]/.test(value);
if (!needsQuoting) return value;
// Double any existing quotes, then wrap in outer quotes
return '"' + value.replace(/"/g, '""') + '"';
}
// Example
const cell = 'He said, "Hello
world"';
console.log(escapeCell(cell));
// → "He said, ""Hello
world""" (Excel-safe)My Recommendation
Always prepend the UTF-8 BOM to any CSV file destined for Excel consumption. It is a 3-byte addition that prevents an entire category of support tickets. Add the BOM stripping guard to catch double-BOM scenarios from pipeline concatenation.
Use CRLF line endings, quote all cells containing commas or newlines, and double any internal quote characters. These four rules make your CSV files robust across Excel, Google Sheets, LibreOffice, and every CSV parser simultaneously.
If you are building a CSV pipeline that also needs to output JSON, our offline CSV to JSON guide covers browser-based stream parsing for large files without hitting the V8 memory limit. For understanding data format choices more broadly, see our XML vs JSON comparison — the format you choose at the API level determines what encoding problems you inherit.
Convert or process CSVs locally.
CSV to JSON conversion with full UTF-8 and BOM support — in your browser, processed locally.
Open CSV to JSON Converter →