Binary Data

Working with binary data in JavaScript

Bun implements several data types and utilities for working with binary data in JavaScript, most of which are Web-standard. Bun-specific APIs are noted as such.

This cheat sheet doubles as a table of contents; click a class in the left column to jump to its section.

ClassDescription
TypedArrayA family of classes that provide an Array-like interface for interacting with binary data. Includes Uint8Array, Uint16Array, Int8Array, and more.
BufferA subclass of Uint8Array that implements a wide range of convenience methods. Unlike the others in this table, it's a Node.js API (which Bun implements). It isn't available in the browser.
DataViewA class that provides a get/set API for writing some number of bytes to an ArrayBuffer at a particular byte offset. Often used for reading or writing binary protocols.
BlobA readonly blob of binary data usually representing a file. Has a MIME type, a size, and methods for converting to ArrayBuffer, ReadableStream, and string.
FileA subclass of Blob that represents a file. Has a name and lastModified timestamp. Node.js v20 has experimental support.
BunFileBun only. A subclass of Blob that represents a lazily-loaded file on disk. Created with Bun.file(path).

ArrayBuffer and views#

JavaScript had no language-native way to store and manipulate binary data until ECMAScript 2015 (ES6) introduced a range of mechanisms for it. The most fundamental building block is ArrayBuffer, a data structure that represents a sequence of bytes in memory.

// this buffer can store 8 bytes
const buf = new ArrayBuffer(8);

Despite the name, it isn't an array and supports none of the array methods and operators you might expect. You can't read or write values from an ArrayBuffer directly; all you can do is check its size and create "slices" from it.

const buf = new ArrayBuffer(8);
buf.byteLength; // => 8

const slice = buf.slice(0, 4); // returns new ArrayBuffer
slice.byteLength; // => 4

To read or write the data, you need a "view": a class that wraps an ArrayBuffer instance and lets you read and manipulate the underlying data. There are two types of views: typed arrays and DataView.

DataView#

The DataView class is a lower-level interface for reading and manipulating the data in an ArrayBuffer.

The following creates a DataView and sets the first byte to 3.

const buf = new ArrayBuffer(4);
// [0b00000000, 0b00000000, 0b00000000, 0b00000000]

const dv = new DataView(buf);
dv.setUint8(0, 3); // write value 3 at byte offset 0
dv.getUint8(0); // => 3
// [0b00000011, 0b00000000, 0b00000000, 0b00000000]

Next, write a Uint16 at byte offset 1. This requires two bytes. The value 513 is 2 * 256 + 1; in bytes, that's 00000010 00000001.

dv.setUint16(1, 513);
// [0b00000011, 0b00000010, 0b00000001, 0b00000000]

console.log(dv.getUint16(1)); // => 513

The first three bytes of the underlying ArrayBuffer now have values. Even though you wrote the second and third bytes with setUint16(), you can still read each component byte with getUint8().

console.log(dv.getUint8(1)); // => 2
console.log(dv.getUint8(2)); // => 1

Writing a value that needs more space than the underlying ArrayBuffer has throws an error. The following writes a Float64 (which requires 8 bytes) at byte offset 0, but the buffer is only four bytes long.

dv.setFloat64(0, 3.1415);
// ^ RangeError: Out of bounds access

The following methods are available on DataView:

TypedArray#

Typed arrays are a family of classes that provide an Array-like interface for interacting with data in an ArrayBuffer. Whereas a DataView lets you write numbers of varying size at a particular offset, a TypedArray interprets the underlying bytes as an array of numbers, each of a fixed size.

It's common to refer to this family of classes collectively by their shared superclass TypedArray. This class is internal to JavaScript; you can't directly create instances of it, and TypedArray is not defined in the global scope. Think of it as an interface or an abstract class.

const buffer = new ArrayBuffer(3);
const arr = new Uint8Array(buffer);

// contents are initialized to zero
console.log(arr); // Uint8Array(3) [0, 0, 0]

// assign values like an array
arr[0] = 0;
arr[1] = 10;
arr[2] = 255;
arr[3] = 255; // no-op, out of bounds

The typed array classes, and how each interprets the bytes in an ArrayBuffer:

ClassDescription
Uint8ArrayEvery one (1) byte is interpreted as an unsigned 8-bit integer. Range 0 to 255.
Uint16ArrayEvery two (2) bytes are interpreted as an unsigned 16-bit integer. Range 0 to 65535.
Uint32ArrayEvery four (4) bytes are interpreted as an unsigned 32-bit integer. Range 0 to 4294967295.
Int8ArrayEvery one (1) byte is interpreted as a signed 8-bit integer. Range -128 to 127.
Int16ArrayEvery two (2) bytes are interpreted as a signed 16-bit integer. Range -32768 to 32767.
Int32ArrayEvery four (4) bytes are interpreted as a signed 32-bit integer. Range -2147483648 to 2147483647.
Float16ArrayEvery two (2) bytes are interpreted as a 16-bit floating point number. Range -6.55e4 to 6.55e4.
Float32ArrayEvery four (4) bytes are interpreted as a 32-bit floating point number. Range -3.4e38 to 3.4e38.
Float64ArrayEvery eight (8) bytes are interpreted as a 64-bit floating point number. Range -1.7e308 to 1.7e308.
BigInt64ArrayEvery eight (8) bytes are interpreted as a signed BigInt. Range -9223372036854775808 to 9223372036854775807 (though BigInt is capable of representing larger numbers).
BigUint64ArrayEvery eight (8) bytes are interpreted as an unsigned BigInt. Range 0 to 18446744073709551615 (though BigInt is capable of representing larger numbers).
Uint8ClampedArraySame as Uint8Array, but automatically "clamps" to the range 0-255 when assigning a value to an element.

The following table shows how different typed array classes interpret the same bytes in an ArrayBuffer.

Byte 0Byte 1Byte 2Byte 3Byte 4Byte 5Byte 6Byte 7
ArrayBuffer0000000000000001000000100000001100000100000001010000011000000111
Uint8Array01234567
Uint16Array256 (1 * 256 + 0)770 (3 * 256 + 2)1284 (5 * 256 + 4)1798 (7 * 256 + 6)
Uint32Array50462976117835012
BigUint64Array506097522914230528n

To create a typed array from a pre-defined ArrayBuffer:

// create typed array from ArrayBuffer
const buf = new ArrayBuffer(10);
const arr = new Uint8Array(buf);

arr[0] = 30;
arr[1] = 60;

// all elements are initialized to zero
console.log(arr); // => Uint8Array(10) [ 30, 60, 0, 0, 0, 0, 0, 0, 0, 0 ];

Instantiating a Uint32Array from this same ArrayBuffer throws an error.

const buf = new ArrayBuffer(10);
const arr = new Uint32Array(buf);
//          ^  RangeError: ArrayBuffer length minus the byteOffset
//             is not a multiple of the element size

A Uint32 value requires four bytes (32 bits). Because the ArrayBuffer is 10 bytes long, there's no way to cleanly divide its contents into 4-byte chunks.

To fix this, create a typed array over a particular "slice" of the ArrayBuffer. The following Uint32Array only "views" the first 8 bytes of the underlying ArrayBuffer: a byteOffset of 0 and a length of 2, the number of Uint32 values the array holds.

// create typed array from ArrayBuffer slice
const buf = new ArrayBuffer(10);
const arr = new Uint32Array(buf, 0, 2);

/*
  buf    _ _ _ _ _ _ _ _ _ _    10 bytes
  arr   [_______,_______]       2 4-byte elements
*/

arr.byteOffset; // 0
arr.length; // 2

You don't need to create an ArrayBuffer instance first; pass a length to the typed array constructor instead:

const arr2 = new Uint8Array(5);

// all elements are initialized to zero
// => Uint8Array(5) [0, 0, 0, 0, 0]

You can also instantiate typed arrays directly from an array of numbers, or another typed array:

// from an array of numbers
const arr1 = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);
arr1[0]; // => 0;
arr1[7]; // => 7;

// from another typed array
const arr2 = new Uint8Array(arr1);

Typed arrays provide the same methods as regular arrays, with a few exceptions. For example, push and pop are not available, because they would require resizing the underlying ArrayBuffer.

const arr = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]);

// supports common array methods
arr.filter(n => n > 5); // Uint8Array(2) [6, 7]
arr.map(n => n * 2); // Uint8Array(8) [0, 2, 4, 6, 8, 10, 12, 14]
arr.reduce((acc, n) => acc + n, 0); // 28
arr.forEach(n => console.log(n)); // 0 1 2 3 4 5 6 7
arr.every(n => n < 10); // true
arr.find(n => n > 5); // 6
arr.includes(5); // true
arr.indexOf(5); // 5

See the MDN documentation for more on typed array properties and methods.

Uint8Array#

Uint8Array is the most common typed array in JavaScript. It represents a classic "byte array": a sequence of 8-bit unsigned integers between 0 and 255.

In Bun, it has methods for converting between byte arrays and their base64 or hex string representations.