Convert a JavaScript Blob to a Base64 String

In illustration of encoding a PNG file as Base64

APIs don't always give you data in the most useful format. I'll show you how to turn a binary JavaScript blob into a usable Base64 data URL.

Sometimes APIs return binary data. With the JavaScript Fetch API, this means that you're getting back a Blob. A Blob is a JavaScript object that encapsulates binary data along with some metadata like size and type. Here's an example using the back-end from one of my projects.

A demonstration of receiving a blob from an API in the FireFox console

This blob contains the binary data for a PNG image. I'd like to use this image as an image.src attribute, but the HTML image element doesn't understand blobs. But it does take data URLs.

A Data URL is a string that contains type information and binary data encoded as Base64. Base64 is a URL-safe format for encoding arbitrary binary data. You can read more about this format here.

Our Blob already has everything we need: binary data and type information. It's just not formatted correctly. Let's write a function to convert our blob into a Base64 Data URL so we can display our image.

Using the JavaScript FileReader

This JS interface lets us convert binary data to Base64

The FileReader is an object that we can use to convert our blob into a Base64 string. There's a member function that looks like it does exactly what we want: FileReader.readAsDataURL(). This method even takes a Blob as an argument!

Using the .readAsDataURL function on a FileReader object, and it returns undefined

🤯 What's going on here? When we create a FileReader and run the readAsDataURL method on our blob, the it returns undefined.

If we look at the documentation for FileReader, we see that this function doesn't return anything. The way to get the result is to listen to Events that are dispatched from FileReader. But what are events? The Events API is one of the main ways that JavaScript facilitates asynchronous operation. Instead of returning, the readAsDataURL function dispatches and Event object that we need to listen for.

So how do we listen to these events? The FileReader has a loadend event that we can use. We provide a callback function, and that function is called whenever the loadend event is dispatched. In our callback function, we get an Event object that has an attribute target from which we can get the result. The target is the object that dispatched the event. In our case, it should be our original FileReader object.

Let's see test it out. We can register a simple callback to print the result.

We registered an onloadend callback. Then called reader.readAsDataURL and can see our callback being called.

Now we can see that our callback was called after we called readAsDataURL once the result was ready. We can see that a Base64 string was printed.

This interface isn't ideal though. It would be nice to call a function and just get our Base64 result inline. Because of JavaScript's asynchronous nature, we're going to need to learn a little about async functions before we can clean this interface up.

Asynchronous Functions and the Promise Interface

One way to write asynchronous code in JavaScript is to use a Promise. A promise is just an object that promises to return a result later. It let's us write some code that uses promises.

let promise = new Promise((resolve, error) => {
  try {
    resolve("The Result");
  } catch(e) {
    error("Something bad happened: " + e);
  }
})
promise.then((r) => console.log(r));

This code looks complicated, but it doesn't do much. It just prints "The Result". The key here is that the callback that we pass to then won't get called until our promise resolves.

JavaScript has always been asynchronous by nature, but ES6 introduced new keywords async and await that allow us to be more explicit when we write asynchronous code.

The async keyword allows us to create functions that return promises, but we don't have to explicitly create any promises. Let's rewrite the above example using the async keyword.

async function returnResult() {
  return "The Result";
}

let promise = returnResult();
promise.then((r) => console.log(r));

This code does the same thing as the promise example, but it's better. It's way less code and clearer to read.

That last line where we use then is still a little ugly. The then syntax gets even more complicated when you have multiple promises to wait on. Your code can become difficult to read with lots of nested then statements. Let's clean it up with await.

The await keyword allows us to wait for the result of a promise inline without passing around callback functions. We can modify the above code to use await like this.

let promise = returnResult();
console.log(await promise);

Or even just this.

console.log(await returnResult());

We don't need to use async to make a function that is awaitable. We can also simply return a promise.

function returnResult() {
  return new Promise((resolve, _) => {
    resolve("The Result");
  });
}

console.log(await returnResult());

Now we have all of the pieces we need write our blobToBase64 function.

Pulling It All Together

Let's use what we know about the FileReader, Events, and Promises to write one async function

With what we've learned about Promises, we can make a more usable interface where we don't need to listen to FileReader's events. We want to pass in our Blob and get back a string encoded in Base64, and we don't want to deal with instantiating a FileReader object and listening to events.

Our goal is to write a function that returns a Promise which resolves when FileReader's loadend event fires. Let's start by scaffolding out our function and creating the FileReader.

function blobToBase64(blob) {
  const reader = new FileReader();
  return new Promise((resolve, _) => {
      // In here, we'll need to use the onloadend
      // callback to resolve our Promise
  });
  // Pass the Blob to FileReader so onloadend can be triggered
}

Now that we have our scaffolding, let's focus on resolving that promise. We know we need to use the onloadend callback. When we get the result from the loadend event, that's the resolution of our promise.

function blobToBase64(blob) {
  const reader = new FileReader();
  return new Promise((resolve, _) => {
    reader.onloadend = (event) => {
      const result = event.target.result
      resolve(result.replace(/^data:.+;base64,/, ''))
    }
    // Pass the Blob to FileReader so onloadend can be triggered
  });
}

Now that we have the FileReader set up and the Promise set to resolve whenever onloadend is dispatched, we just need to pass the blob to our reader.

function blobToBase64(blob) {
  const reader = new FileReader();
  return new Promise((resolve, _) => {
    reader.onloadend = (event) => {
      const result = event.target.result
      resolve(result.replace(/^data:.+;base64,/, ''))
    }
    reader.readAsDataURL(blob);
  });
}

That's it! We have our one function. We can call it like this.

blob = await response.blob();
base64String = await blobToBase64(blob);

We did it 🥳