7

I have implemented the following code to parse a CSV via a <input type="file" /> selection:

export async function parse(file: File) {
  let content = '';
  const reader = new FileReader();
  reader.onload = function(e: any) {
    content = e.target.result;
  };
  await reader.readAsText(file);
  const result = content.split(/\r\n|\n/);
  return result;
}

If I run this code and put a breakpoint on the line where I declare result, it retrieves the contents of the file successfully. If I do not put any breakpoint, the content is empty. As you can see, I added await to the line where the reader reads the file as text, but it's still not working.

Bharata
  • 13,509
  • 6
  • 36
  • 50
mellis481
  • 4,332
  • 12
  • 71
  • 118
  • Instead of returning the result, you should return a Promise, and resolve it in the `onload` listener – Seblor Jun 25 '18 at 14:59
  • `await` doesn't help here. `readAsText()` doesn't return a `Promise`. – zero298 Jun 25 '18 at 15:00
  • FileReader is not return a promise try loadend event here is [MDN](https://developer.mozilla.org/zh-TW/docs/Web/Events/loadend) – jerry_p Jun 25 '18 at 15:04

3 Answers3

19

await doesn't help here. readAsText() doesn't return a Promise.

You need to wrap the whole process in a Promise:

export function parse(file: File) {
  // Always return a Promise
  return new Promise((resolve, reject) => {
    let content = '';
    const reader = new FileReader();
    // Wait till complete
    reader.onloadend = function(e: any) {
      content = e.target.result;
      const result = content.split(/\r\n|\n/);
      resolve(result);
    };
    // Make sure to handle error states
    reader.onerror = function(e: any) {
      reject(e);
    };
    reader.readAsText(file);
  });
}
zero298
  • 25,467
  • 10
  • 75
  • 100
  • Got it. Thanks! – mellis481 Jun 25 '18 at 15:09
  • 2
    I think you should use `onload` instead of `onloadend` here, to avoid getting duplicate callbacks for errors. The docs for `onloadend` say _A handler for the loadend event. This event is triggered each time the reading operation is completed (**either in success or failure**)._ https://developer.mozilla.org/sv-SE/docs/Web/API/FileReader – MEMark May 24 '20 at 12:12
  • Why did you add `reader.readAsText(file);` at the end ? Doesn't this have to be at the beginning so that we have a file and the text from it ? How does `content = e.target.result;` work if there is no `readAsText(file)` ? – Jorje12 Jul 30 '20 at 21:07
  • @Jorje12 No and yes. We aren't doing anything with the file until we call `reader.readAsText()`. The `reader.on*` parts are setting the event handlers. You have to set the listeners before you start reading or you're going to miss the timing. It's almost the exact same pattern that you have to follow to deal with `Image` loading. See my answer here: [loading an image on web browser using Promise()](https://stackoverflow.com/a/52060802/691711). – zero298 Jul 30 '20 at 21:36
  • That makes sense. I need to read more on the events though. Thanks for the quick answer. – Jorje12 Jul 30 '20 at 21:37
6

Here is the JSBin I have tried and it work like a charm.

function parse(file) {
  const reader = new FileReader();
  reader.readAsText(file);
  reader.onload = function(event) {
    // The file's text will be printed here
  console.log(reader.result)
  }
}

Updated:

I write you a Promise version.

async function parse(file) {
  const reader = new FileReader();
  reader.readAsText(file);
  const result = await new Promise((resolve, reject) => {
    reader.onload = function(event) {
    resolve(reader.result)
    }
  })
  console.log(result)
}
jerry_p
  • 201
  • 1
  • 10
  • 3
    `async` is adding unnecessary complexity here. `async` will return a `Promise` no matter what so you might as well return the `Promise` you already constructed. Otherwise, you are unwrapping the `Promise` and then immediately wrapping in a `Promise` again. – zero298 Jun 25 '18 at 15:32
  • @zero298 You’re right async is not necessary in this case thanks for your advice. – jerry_p Jun 26 '18 at 00:28
  • Shouldn't the event handler `reader.onload` be set before calling `reader.readAsText()` to avoid a race condition? – MEMark May 24 '20 at 12:08
1

To generalize @zero298's answer a tiny bit, here's the generic Promise-based wrapper around FileReader -

// get contents of a file as obtained from an html input type=file element
function getFileContents(file) {
  return new Promise((resolve, reject) => {
    let contents = ""
    const reader = new FileReader()
    reader.onloadend = function (e) {
      contents = e.target.result
      resolve(contents)
    }
    reader.onerror = function (e) {
      reject(e)
    }
    reader.readAsText(file)
  })
}

used like so -

async function parse(file) {
  const contents = await getFileContents(file)
  const result = contents.split(/\r\n|\n/)
  return result
}

or in the general case,

async function show(file) {
  const contents = await getFileContents(file)
  alert(contents)
}
Brian Burns
  • 20,575
  • 8
  • 83
  • 77
  • I think you should use `onload` instead of `onloadend` here, to avoid getting duplicate callbacks for errors. The docs for `onloadend` say _A handler for the loadend event. This event is triggered each time the reading operation is completed (**either in success or failure**)._ https://developer.mozilla.org/sv-SE/docs/Web/API/FileReader – MEMark May 24 '20 at 12:13