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 0xA9 in 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.
1# File without BOM (breaks Excel)24e,61,6d,65,2c,45,6d,61,69,6c → Name,Email352,65,6e,c3,a9,2c,72,65,6e,65 → René (broken!)4 5# File with BOM (Excel reads correctly)6ef,bb,bf,4e,61,6d,65,2c,45,6d → [BOM]Name,Email761,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:
1// Step 1: Build your CSV string as UTF-8 text2const rows = [3 ['Name', 'Email', 'City'],4 ['René', 'rene@example.com', 'Paris'],5 ['José', 'jose@example.com', 'Madrid'],6];7const csvContent = rows.map(row =>8 row.map(cell => `"${cell.replace(/"/g, '""')}"`).join(',')9).join('\n');10 11// Step 2: Prepend the UTF-8 BOM (0xEF 0xBB 0xBF)12const bom = new Uint8Array([0xEF, 0xBB, 0xBF]);13 14// Step 3: Combine BOM + CSV content into a Blob15const blob = new Blob([bom, csvContent], {16 type: 'text/csv;charset=utf-8;'17});18 19// Step 4: Trigger download20const url = URL.createObjectURL(blob);21const link = document.createElement('a');22link.href = url;23link.download = 'contacts-utf8.csv';24link.click();25 26// Step 5: Clean up object URL to avoid memory leak27URL.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:
1import { writeFileSync } from 'fs';2 3const BOM = ''; // UTF-8 BOM as Unicode escape4const csvContent = 'Name,Email\nRené,rene@example.com\nJosé,jose@example.com';5 6// Prepend BOM before writing7writeFileSync('output.csv', BOM + csvContent, { encoding: 'utf8' });8 9// Verify: the resulting file starts with bytes EF BB BF10// 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.
1// Detect and strip existing BOM before prepending2function addBomSafely(csvString: string): string {3 const BOM = '';4 // Strip any existing BOM first5 const stripped = csvString.startsWith(BOM)6 ? csvString.slice(1)7 : csvString;8 return BOM + stripped;9}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.
1# Trailing comma — creates an empty 4th column2Name,Email,City,3René,rene@example.com,Paris, ← extra empty cell4 5# Correct — no trailing comma6Name,Email,City7René,rene@example.com,ParisThis 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.
1// Force CRLF line endings for maximum Excel compatibility2const csvContent = rows3 .map(row => row.join(','))4 .join('
5'); // RFC 4180 compliant — not just '6'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.
1// Safe cell escape function — handles commas, newlines, quotes2function escapeCell(value: string): string {3 const needsQuoting = /[,"4
]/.test(value);5 if (!needsQuoting) return value;6 // Double any existing quotes, then wrap in outer quotes7 return '"' + value.replace(/"/g, '""') + '"';8}9 10// Example11const cell = 'He said, "Hello12world"';13console.log(escapeCell(cell));14// → "He said, ""Hello15world""" (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.
Prepend the UTF-8 BOM
Write the bytes 0xEF 0xBB 0xBF at the start so Excel reads the file as UTF-8 instead of Windows-1252.
Guard against a double BOM
Strip any existing BOM before prepending, or cell A1 shows an invisible that breaks header matching and VLOOKUP.
Use CRLF line endings
RFC 4180 specifies \r\n. It is the most compatible choice across Excel on Windows and macOS.
Quote cells with commas or newlines
Wrap the value in double quotes and double any internal quote characters.
Verify the leading bytes
Confirm the file begins with EF BB BF, e.g. xxd output.csv | head -1.
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, no upload required.
Open CSV to JSON Converter →