FileReader in JavaScript: read a file the user picked

FileReader turns a File from an input or a drop into text, a data: URL or raw bytes, all inside the page. The result arrives later, in the load event, which is where most first attempts go wrong.

FileReader reads a file the user picked, in the browser, without uploading it. Make a reader, set onload, then call one read method. When the load event fires, reader.result holds the file as text, as a data: URL, or as raw bytes.

Try it. Pick a text or CSV file, or press the sample button if you have none at hand.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>FileReader readAsText</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
  input[type=file] { max-width: 100%; }
  button { font: inherit; padding: 6px 12px; border-radius: 8px; border: 1px solid #c9ced8; background: #fff; cursor: pointer; }
  #info { margin: 12px 0 6px; font-size: 14px; }
  #info.err { color: #b42318; }
  pre { margin: 0; height: 170px; overflow: auto; padding: 10px 12px; background: #fff; border: 1px solid #e1e4ea;
        border-radius: 8px; font: 13px/1.45 ui-monospace, Consolas, monospace; white-space: pre-wrap; word-break: break-word; }
</style>
</head>
<body>
<div class="bar">
  <input type="file" id="pick" accept=".txt,.csv,.json,.md,.html,text/*">
  <button id="sample" type="button">Use a sample file</button>
</div>
<p id="info">Pick a text file. Nothing is uploaded; the file is read in this page.</p>
<pre id="out"></pre>

<script>
  const pick = document.getElementById('pick');
  const info = document.getElementById('info');
  const out = document.getElementById('out');

  function read(file) {
    const reader = new FileReader();

    // The result exists only once the load event fires
    reader.onload = () => {
      info.className = '';
      info.textContent = `${file.name}: ${file.size} bytes, ${reader.result.length} characters`;
      out.textContent = reader.result;  // textContent shows the file as text, never runs it as HTML
    };

    reader.onerror = () => {
      info.className = 'err';
      info.textContent = `Could not read ${file.name}: ${reader.error.name}`;
    };

    reader.readAsText(file);  // decodes as UTF-8 unless you pass an encoding
  }

  pick.addEventListener('change', () => {
    if (pick.files.length) read(pick.files[0]);
  });

  // A File made in code, for trying the demo without a file of your own
  document.getElementById('sample').addEventListener('click', () => {
    read(new File(['name,city\nAna,Lisbon\nJoão,Porto\n'], 'sample.csv', { type: 'text/csv' }));
  });
</script>
</body>
</html>
readAsText in about 15 lines. The file stays in the page; the sample button builds a File in code.

The sample line João,Porto shows a detail worth knowing early. The file is 33 bytes but the text is 32 characters, because ã takes two bytes in UTF-8. file.size counts bytes; result.length counts characters.

The code in four lines

const reader = new FileReader();
reader.onload = () => show(reader.result);
reader.onerror = () => alert(reader.error.name);
reader.readAsText(file);

file comes from input.files[0] in a change listener, or from event.dataTransfer.files in a drop. Building the drop zone itself is covered in drag and drop file upload.

Set the handlers before the read call. The order does not change when load fires, but it keeps the code readable top to bottom.

Why reader.result is null

The read methods do not return the file. They start reading and return at once, with undefined. The rest of your function keeps running while the browser reads in the background.

Reading reader.result on the next line gives null. Inside onload it holds the file.
Reading reader.result on the next line gives null. Inside onload it holds the file.

So reader.result is null on the line after readAsText. It is set just before load fires. Anything that needs the contents, such as showing text or parsing rows, goes inside onload or a function called from it.

readAsText, readAsDataURL, readAsArrayBuffer

The three methods read the same bytes and hand back three kinds of value. Pick the one that matches what you will do next.

One 2-byte file, three results. Measured in Chrome, Firefox and Safari's engine.
One 2-byte file, three results. Measured in Chrome, Firefox and Safari's engine.
Method reader.result Use it for
readAsText(file) A string .txt, .csv, .json, .md, .html
readAsText(file, 'windows-1252') A string, decoded with that encoding Old files that are not UTF-8
readAsDataURL(file) A data: string with base64 An image src, or storing a file as text
readAsArrayBuffer(file) An ArrayBuffer of bytes Checking file headers, binary formats

A file with no type gets a generic type in front of the data:

data:application/octet-stream;base64,aGk=

There is also readAsBinaryString, kept for older code. MDN points to readAsArrayBuffer instead.

onload, onerror and the other events

A reader moves through three readyState values and fires events on the way.

EMPTY, LOADING, DONE. load or error decides what result holds; loadend comes last either way.
EMPTY, LOADING, DONE. load or error decides what result holds; loadend comes last either way.
  • load - the read worked. reader.result is ready.
  • error - the read failed. reader.error is a DOMException; its name says why.
  • progress - fires while a file is read. event.loaded and event.total are in bytes, enough for a progress bar on a large file.
  • abort - your code called reader.abort().
  • loadend - fires after load, error or abort. Good for hiding a spinner.

Errors are rare but real. In our test, a file deleted after it was picked failed with NotFoundError in all three browser engines.

file.text() and the promise versions

Every File is a Blob, and Blob has promise methods that do the same jobs with less code:

const text = await file.text();          // like readAsText, always UTF-8
const bytes = await file.arrayBuffer();  // like readAsArrayBuffer
FileReader file.text() / file.arrayBuffer()
Style Events: onload, onerror A promise you can await
Choose the text encoding Yes, second argument No, always UTF-8
Data URL readAsDataURL No direct method
Progress events Yes No
Errors onerror The promise rejects; use try/catch

For UTF-8 text, await file.text() is the short way. If await is new to you, async and await explains it. FileReader earns its place for other encodings, data URLs and progress.

To use FileReader with await anyway, wrap it once:

function readText(file, encoding) {
  return new Promise((resolve, reject) => {
    const r = new FileReader();
    r.onload = () => resolve(r.result);
    r.onerror = () => reject(r.error);
    r.readAsText(file, encoding);
  });
}

Previewing an image: data URL or object URL

For an image preview you need something to put in img.src. readAsDataURL gives a data: string. URL.createObjectURL(file) gives a short blob: address. Both show the same picture.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Image preview: readAsDataURL vs createObjectURL</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
  input[type=file] { max-width: 100%; }
  button { font: inherit; padding: 6px 12px; border-radius: 8px; border: 1px solid #c9ced8; background: #fff; cursor: pointer; }
  #sig { margin: 12px 0; font-size: 14px; }
  #sig code { font: 13px ui-monospace, Consolas, monospace; background: #fff; padding: 1px 5px; border-radius: 4px; }
  .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
  figure { margin: 0; padding: 10px; background: #fff; border: 1px solid #e1e4ea; border-radius: 10px; min-width: 0; }
  figcaption { font: 600 13px ui-monospace, Consolas, monospace; margin-bottom: 8px; }
  .box { height: 150px; display: grid; place-items: center; background: #eef1f5; border-radius: 6px; overflow: hidden; }
  img { max-width: 100%; max-height: 150px; }
  small { display: block; margin-top: 8px; font-size: 12px; color: #4b5563; overflow-wrap: anywhere; }
</style>
</head>
<body>
<div class="bar">
  <input type="file" id="pick" accept="image/*">
  <button id="sample" type="button">Use a sample image</button>
</div>
<p id="sig">Pick an image. The first bytes of the file are checked too.</p>
<div class="grid">
  <figure><figcaption>readAsDataURL</figcaption><div class="box"><img id="a" alt=""></div><small id="aSrc">src: none yet</small></figure>
  <figure><figcaption>createObjectURL</figcaption><div class="box"><img id="b" alt=""></div><small id="bSrc">src: none yet</small></figure>
</div>

<script>
  const pick = document.getElementById('pick');
  let lastUrl = null;

  function show(file) {
    // 1. FileReader: the whole file becomes a base64 data: string
    const reader = new FileReader();
    reader.onload = () => {
      document.getElementById('a').src = reader.result;
      document.getElementById('aSrc').textContent =
        `src: ${reader.result.slice(0, 30)}... (${reader.result.length} characters)`;
    };
    reader.readAsDataURL(file);

    // 2. Object URL: a short blob: address, ready at once. Free the previous one.
    if (lastUrl) URL.revokeObjectURL(lastUrl);
    lastUrl = URL.createObjectURL(file);
    document.getElementById('b').src = lastUrl;
    document.getElementById('bSrc').textContent = `src: ${lastUrl} (${lastUrl.length} characters)`;

    // 3. readAsArrayBuffer: look at the real bytes, not the file name
    const bytes = new FileReader();
    bytes.onload = () => {
      const head = [...new Uint8Array(bytes.result)]
        .map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ');
      let kind = 'not a PNG, JPEG or GIF';
      if (head.startsWith('89 50 4E 47')) kind = 'PNG';
      else if (head.startsWith('FF D8 FF')) kind = 'JPEG';
      else if (head.startsWith('47 49 46 38')) kind = 'GIF';
      document.getElementById('sig').innerHTML =
        `<b>${file.size}</b> bytes. First bytes <code>${head}</code>: ${kind}`;
    };
    bytes.readAsArrayBuffer(file.slice(0, 4));  // read only the first 4 bytes
  }

  pick.addEventListener('change', () => {
    if (pick.files.length) show(pick.files[0]);
  });

  // Draw a small picture on a canvas and turn it into a PNG File
  document.getElementById('sample').addEventListener('click', () => {
    const c = document.createElement('canvas');
    c.width = 240; c.height = 160;
    const g = c.getContext('2d');
    const grad = g.createLinearGradient(0, 0, 240, 160);
    grad.addColorStop(0, '#2563eb'); grad.addColorStop(1, '#16a34a');
    g.fillStyle = grad; g.fillRect(0, 0, 240, 160);
    g.fillStyle = '#fff'; g.font = 'bold 28px system-ui, sans-serif'; g.fillText('sample.png', 34, 90);
    c.toBlob(blob => show(new File([blob], 'sample.png', { type: 'image/png' })), 'image/png');
  });
</script>
</body>
</html>
Left: readAsDataURL. Right: createObjectURL. The line above uses readAsArrayBuffer on the first 4 bytes.

With the 47,721-byte sample PNG, the data URL was 63,650 characters long. Base64 writes every 3 bytes as 4 characters. The blob URL stayed at 46 characters. For previews on screen, the object URL is the lighter choice.

A data URL wins when the image must be kept as text: saved inside an HTML file, or put in JSON. Blob URLs covers object URLs, including when to call revokeObjectURL.

The demo also reads the first 4 bytes with file.slice(0, 4) and readAsArrayBuffer.

Real PNG files start with 89 50 4E 47, JPEG with FF D8 FF. A text file renamed to .png fails this check, while file.type still says image/png, because the browser takes it from the name.

Choosing the text encoding

readAsText decodes as UTF-8 unless you pass an encoding label. Some older Windows programs save text in Windows-1252, where é is one byte. Read as UTF-8, that byte turns into the replacement character �.

reader.readAsText(file, 'windows-1252');

Two behaviours we measured, the same in Chrome, Firefox and Safari's engine:

  • A byte order mark wins. If the file starts with the UTF-8 mark (EF BB BF), it is read as UTF-8 whatever label you pass, and the mark is dropped from the text.
  • Unknown labels fall back to UTF-8. A typo such as 'windows1252' gives no error, just UTF-8 decoding.

Nothing in the file says which encoding it uses without a mark, so the demo below lets the reader switch and re-read.

Reading a CSV into a table

The finished example reads a CSV with readAsText, splits it into rows and cells, and builds a table. The encoding menu re-reads the same file.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Read a CSV file into a table</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .bar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; font-size: 14px; }
  input[type=file] { max-width: 100%; }
  button, select { font: inherit; padding: 6px 10px; border-radius: 8px; border: 1px solid #c9ced8; background: #fff; }
  button { cursor: pointer; }
  #info { margin: 12px 0 8px; font-size: 14px; }
  #info.err { color: #b42318; }
  .wrap { max-height: 250px; overflow: auto; background: #fff; border: 1px solid #e1e4ea; border-radius: 8px; }
  table { border-collapse: collapse; font-size: 14px; min-width: 100%; }
  th, td { padding: 7px 10px; border-bottom: 1px solid #eef0f3; text-align: left; white-space: nowrap; }
  th { position: sticky; top: 0; background: #eef1f5; }
</style>
</head>
<body>
<div class="bar">
  <input type="file" id="pick" accept=".csv,text/csv">
  <button id="sample" type="button">Use a sample CSV</button>
  <label>Encoding
    <select id="enc">
      <option value="utf-8">UTF-8</option>
      <option value="windows-1252">Windows-1252</option>
    </select>
  </label>
</div>
<p id="info">Pick a .csv file, or use the sample.</p>
<div class="wrap"><table id="table"></table></div>

<script>
  const pick = document.getElementById('pick');
  const enc = document.getElementById('enc');
  const info = document.getElementById('info');
  const table = document.getElementById('table');
  let current = null;  // the File on screen, so a new encoding can re-read it

  // Split CSV text into rows of cells. Handles "quoted, cells" and "" inside quotes.
  function parseCSV(text, sep) {
    const rows = [[]];
    let cell = '', quoted = false;
    for (let i = 0; i < text.length; i++) {
      const ch = text[i];
      if (quoted) {
        if (ch === '"' && text[i + 1] === '"') { cell += '"'; i++; }
        else if (ch === '"') quoted = false;
        else cell += ch;
      } else if (ch === '"') quoted = true;
      else if (ch === sep) { rows.at(-1).push(cell); cell = ''; }
      else if (ch === '\n') { rows.at(-1).push(cell); cell = ''; rows.push([]); }
      else if (ch !== '\r') cell += ch;
    }
    rows.at(-1).push(cell);
    return rows.filter(r => r.some(c => c !== ''));  // drop blank lines
  }

  function render(rows) {
    table.replaceChildren();
    rows.forEach((cells, n) => {
      const tr = table.insertRow();
      for (const c of cells) {
        const cellEl = document.createElement(n === 0 ? 'th' : 'td');
        cellEl.textContent = c;  // text only, so a cell cannot inject HTML
        tr.append(cellEl);
      }
    });
  }

  function read(file) {
    current = file;
    const reader = new FileReader();
    reader.onload = () => {
      const text = reader.result;
      const first = text.split('\n')[0];
      // Spreadsheets set to some European locales save with ; instead of ,
      const sep = (first.split(';').length > first.split(',').length) ? ';' : ',';
      const rows = parseCSV(text, sep);
      render(rows);
      info.className = '';
      info.textContent = `${file.name}: ${Math.max(rows.length - 1, 0)} rows, ` +
        `${rows[0] ? rows[0].length : 0} columns, separator "${sep}", read as ${enc.value}`;
    };
    reader.onerror = () => {
      info.className = 'err';
      info.textContent = `Could not read ${file.name}: ${reader.error.name}`;
    };
    reader.readAsText(file, enc.value);  // the second argument picks the text encoding
  }

  pick.addEventListener('change', () => { if (pick.files.length) read(pick.files[0]); });
  enc.addEventListener('change', () => { if (current) read(current); });

  document.getElementById('sample').addEventListener('click', () => {
    const csv = 'Name,City,Order,Note\n' +
      'Ana,Lisbon,12.50,First order\n' +
      'João,Porto,8.00,"Paid, then refunded"\n' +
      'Zoë,Café Street,30.25,"She said ""thanks"""\n';
    read(new File([csv], 'orders.csv', { type: 'text/csv' }));
  });
</script>
</body>
</html>
Pick a .csv or use the sample. Switch to Windows-1252 to see what a wrong encoding does to João and Zoë.
  • Quoted cells: "Paid, then refunded" keeps its comma, and "" inside quotes becomes one ". Splitting on commas alone breaks these rows.
  • Separator: the first line is checked for ; versus ,. Spreadsheets set to some European locales save with semicolons.
  • Line ends: \r is skipped, so Windows (\r\n) and \n line ends both work.
  • Safe output: each cell goes in with textContent, so a cell that contains HTML shows as text.

For files that open wrong in a spreadsheet app, see opening a CSV file. To publish a finished table rather than parse one, see HTML table from CSV.

When it does not work

What you see Cause Fix
reader.result is null Read on the line after the read call Use it inside onload
InvalidStateError A second read started on a busy reader One FileReader per file
é or � in the text Wrong encoding Pass the right label to readAsText
[object ArrayBuffer] on screen Used readAsArrayBuffer for text Use readAsText, or decode with TextDecoder
Image preview is blank Set src to the File object itself Use the data URL or an object URL
onerror with NotFoundError The file moved or was deleted after picking Ask the user to pick it again

A file reader is easier to show than to describe. The person you send it to can pick their own file and watch it turn into a table or a preview, and the file never leaves their device.

To send a working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it. The page renders as written and its scripts run, so the file picker works for whoever opens it.

If you change the code later, the same link shows the new version.

Questions people ask

Why is reader.result null?

Because the read has not finished. readAsText and the other read methods only start reading and return straight away. reader.result is filled in just before the load event fires, so use it inside onload, or await file.text() instead.

Can FileReader read a file from a path such as C:\data\list.csv?

No. A web page cannot open files by path. FileReader reads File or Blob objects only: the ones the user hands over through an <input type="file"> or a drop, and ones your own code creates with new File() or new Blob().

Should I use FileReader or file.text()?

For UTF-8 text, file.text() is shorter: it returns a promise and needs no events. Use FileReader when you need readAsText with another encoding, readAsDataURL, or progress events while a large file is read.

Does FileReader upload the file?

No. Everything happens inside the browser tab. The file only leaves the device if your code sends it somewhere, for example with fetch and FormData.

How do I read several files?

Loop over input.files and give each file its own FileReader. One reader cannot start a second read while the first is still loading; that call throws an InvalidStateError.

Keep reading