Skip to main content

How to Work with ArrayBuffer and Binary Arrays in JavaScript

JavaScript was originally designed for manipulating text, DOM elements, and simple data structures. But modern web applications often need to work with raw binary data: downloading and uploading files, processing images and audio, communicating with hardware, parsing binary file formats, working with WebSockets, and interfacing with WebAssembly. For all of these, JavaScript provides a set of low-level binary data APIs built around ArrayBuffer and typed arrays.

These APIs give you direct access to memory at the byte level, something that is natural in languages like C or Rust but was historically impossible in JavaScript. Understanding how they work opens the door to high-performance data processing, file manipulation, and network communication in the browser and Node.js.

This guide walks you through the entire binary data system: from ArrayBuffer (the raw memory container) through typed arrays (the views that let you read and write specific data types) to DataView (the flexible tool for mixed-format binary data), and finally how to convert between binary data and text.

ArrayBuffer: Raw Binary Data​

An ArrayBuffer is a fixed-length block of raw binary data. Think of it as a chunk of memory with a specific number of bytes. You cannot read or write individual bytes directly on an ArrayBuffer. Instead, it serves as the underlying storage that views (typed arrays and DataView) operate on.

Creating an ArrayBuffer​

// Create a buffer of 16 bytes (all initialized to zero)
let buffer = new ArrayBuffer(16);

console.log(buffer.byteLength); // 16
console.log(buffer); // ArrayBuffer(16)

Every byte in a new ArrayBuffer is initialized to 0. The size is fixed at creation and cannot be changed later.

// The size is specified in bytes
let smallBuffer = new ArrayBuffer(4); // 4 bytes
let mediumBuffer = new ArrayBuffer(256); // 256 bytes
let largeBuffer = new ArrayBuffer(1024); // 1 KB

console.log(smallBuffer.byteLength); // 4
console.log(mediumBuffer.byteLength); // 256
console.log(largeBuffer.byteLength); // 1024

You Cannot Access Bytes Directly​

This is a critical concept. ArrayBuffer itself has no methods for reading or writing data. It is just a container:

let buffer = new ArrayBuffer(8);

// ❌ These do NOT work
// buffer[0] = 42; // Does nothing meaningful
// console.log(buffer[0]); // undefined

To actually work with the data inside an ArrayBuffer, you need a view: either a typed array or a DataView. The ArrayBuffer provides the memory; the view provides the interpretation.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ArrayBuffer β”‚
β”‚ (raw bytes: 00 00 00 00 00 00 00 00) β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Uint8Array view β”‚ β†’ sees 8 bytes β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Uint32Array view β”‚ β†’ sees 2 ints β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ Float64Array view β”‚ β†’ sees 1 float β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Multiple views can share the same underlying ArrayBuffer, and each view interprets the same bytes differently.

Slicing an ArrayBuffer​

The slice method creates a new ArrayBuffer containing a copy of a portion of the original:

let buffer = new ArrayBuffer(16);

// Fill with some data via a view
let view = new Uint8Array(buffer);
for (let i = 0; i < 16; i++) {
view[i] = i * 10; // 0, 10, 20, 30, ... 150
}

// Slice bytes 4 through 7 (exclusive end)
let sliced = buffer.slice(4, 8);
console.log(sliced.byteLength); // 4

let slicedView = new Uint8Array(sliced);
console.log(slicedView); // Uint8Array [40, 50, 60, 70]

// The slice is a COPY: modifying it does not affect the original
slicedView[0] = 255;
console.log(view[4]); // 40 (original unchanged)

ArrayBuffer Is Not Resizable (by Default)​

Once created, an ArrayBuffer cannot grow or shrink:

let buffer = new ArrayBuffer(8);
// buffer.byteLength = 16; // ❌ Cannot change

// To "resize," you must create a new buffer and copy the data
let newBuffer = new ArrayBuffer(16);
new Uint8Array(newBuffer).set(new Uint8Array(buffer));
info

The ArrayBuffer with the maxByteLength option and the resize() method were introduced in recent ECMAScript proposals. However, for most practical work, you treat ArrayBuffer as a fixed-size container and use typed arrays and DataView to interact with its contents.

Typed Arrays: Uint8Array, Int32Array, Float64Array, and More​

Typed arrays are views over an ArrayBuffer that let you read and write data as specific numeric types. Each typed array treats the underlying bytes as a sequence of numbers of a particular format (unsigned 8-bit integers, 32-bit floats, etc.).

The Complete Typed Array Family​

Typed ArrayBytes per ElementValue RangeDescription
Int8Array1-128 to 127Signed 8-bit integer
Uint8Array10 to 255Unsigned 8-bit integer
Uint8ClampedArray10 to 255Clamped (no overflow wrapping)
Int16Array2-32,768 to 32,767Signed 16-bit integer
Uint16Array20 to 65,535Unsigned 16-bit integer
Int32Array4-2,147,483,648 to 2,147,483,647Signed 32-bit integer
Uint32Array40 to 4,294,967,295Unsigned 32-bit integer
Float32Array4~1.2Γ—10⁻³⁸ to ~3.4Γ—10³⁸32-bit floating point
Float64Array8~5Γ—10⁻³²⁴ to ~1.8Γ—10³⁰⁸64-bit floating point (same as JS number)
BigInt64Array8-2⁢³ to 2⁢³-1Signed 64-bit BigInt
BigUint64Array80 to 2⁢⁴-1Unsigned 64-bit BigInt

Creating Typed Arrays​

There are several ways to create a typed array:

From length (creates a new buffer automatically):

// Create a Uint8Array with 4 elements (and a new 4-byte ArrayBuffer underneath)
let bytes = new Uint8Array(4);
console.log(bytes.length); // 4
console.log(bytes.byteLength); // 4 (4 elements Γ— 1 byte each)
console.log(bytes.buffer.byteLength); // 4 (the underlying ArrayBuffer)
console.log(bytes); // Uint8Array [0, 0, 0, 0]

// Create a Float64Array with 3 elements (needs 3 Γ— 8 = 24 bytes)
let floats = new Float64Array(3);
console.log(floats.length); // 3
console.log(floats.byteLength); // 24 (3 elements Γ— 8 bytes each)
console.log(floats); // Float64Array [0, 0, 0]

From an array or iterable:

let bytes = new Uint8Array([10, 20, 30, 40]);
console.log(bytes); // Uint8Array [10, 20, 30, 40]

let ints = new Int32Array([100, -200, 300]);
console.log(ints); // Int32Array [100, -200, 300]

let floats = new Float64Array([1.5, 2.7, 3.14159]);
console.log(floats); // Float64Array [1.5, 2.7, 3.14159]

From an existing ArrayBuffer:

let buffer = new ArrayBuffer(8);

// View the 8 bytes as 8 individual bytes
let uint8 = new Uint8Array(buffer);
console.log(uint8.length); // 8

// View the same 8 bytes as 2 thirty-two-bit integers
let uint32 = new Uint32Array(buffer);
console.log(uint32.length); // 2

// View the same 8 bytes as 1 sixty-four-bit float
let float64 = new Float64Array(buffer);
console.log(float64.length); // 1

// All three views share the SAME underlying buffer
console.log(uint8.buffer === uint32.buffer); // true
console.log(uint32.buffer === float64.buffer); // true

From a buffer with offset and length:

let buffer = new ArrayBuffer(16);

// View bytes 4-11 (8 bytes) as Uint8Array
let partial = new Uint8Array(buffer, 4, 8); // (buffer, byteOffset, length)
console.log(partial.length); // 8
console.log(partial.byteOffset); // 4
console.log(partial.byteLength); // 8

Reading and Writing Data​

Typed arrays support index-based access like regular arrays:

let bytes = new Uint8Array(4);

// Write values
bytes[0] = 72; // 'H' in ASCII
bytes[1] = 101; // 'e'
bytes[2] = 108; // 'l'
bytes[3] = 108; // 'l'

// Read values
console.log(bytes[0]); // 72
console.log(bytes[1]); // 101

// Iterate
for (let byte of bytes) {
console.log(byte); // 72, 101, 108, 108
}

Overflow and Clamping Behavior​

Values that exceed the range of a typed array's element type are handled differently depending on the array type:

Regular typed arrays wrap around (modular arithmetic):

let uint8 = new Uint8Array(1);

uint8[0] = 256; // 256 % 256 = 0
console.log(uint8[0]); // 0

uint8[0] = 257; // 257 % 256 = 1
console.log(uint8[0]); // 1

uint8[0] = -1; // Wraps to 255
console.log(uint8[0]); // 255

let int8 = new Int8Array(1);
int8[0] = 128; // Wraps to -128
console.log(int8[0]); // -128

int8[0] = 200; // Wraps to -56
console.log(int8[0]); // -56

Uint8ClampedArray clamps to the range (no wrapping):

let clamped = new Uint8ClampedArray(1);

clamped[0] = 300;
console.log(clamped[0]); // 255 (clamped to max)

clamped[0] = -50;
console.log(clamped[0]); // 0 (clamped to min)

clamped[0] = 128;
console.log(clamped[0]); // 128 (within range, no change)

Uint8ClampedArray is specifically designed for image pixel data, where clamping is the correct behavior (pixel values must be 0-255).

Multiple Views on the Same Buffer​

Different typed arrays can provide different interpretations of the same bytes:

let buffer = new ArrayBuffer(4);
let uint8 = new Uint8Array(buffer);
let uint32 = new Uint32Array(buffer);

// Write through the Uint32 view
uint32[0] = 0x44434241; // Hex value

// Read through the Uint8 view: see individual bytes
console.log(uint8[0]); // 65 (0x41 = 'A')
console.log(uint8[1]); // 66 (0x42 = 'B')
console.log(uint8[2]); // 67 (0x43 = 'C')
console.log(uint8[3]); // 68 (0x44 = 'D')

// The byte order depends on system endianness (typically little-endian on x86)
// 0x44434241 is stored as bytes: 41 42 43 44 (little-endian)
info

Endianness (byte order) matters when interpreting multi-byte values. Most modern computers use little-endian format, where the least significant byte is stored first. This means 0x0102 is stored as bytes 02 01. Typed arrays use the system's native endianness. If you need to control byte order explicitly (e.g., when reading binary file formats or network protocols), use DataView instead (covered later).

Typed Array Methods​

Typed arrays share many methods with regular arrays, but not all:

let arr = new Uint8Array([50, 10, 40, 20, 30]);

// Familiar array methods that work:
arr.sort();
console.log(arr); // Uint8Array [10, 20, 30, 40, 50]

let found = arr.find(x => x > 25);
console.log(found); // 30

let filtered = arr.filter(x => x >= 30);
console.log(filtered); // Uint8Array [30, 40, 50]

let mapped = arr.map(x => x * 2);
console.log(mapped); // Uint8Array [20, 40, 60, 80, 100]

console.log(arr.includes(30)); // true
console.log(arr.indexOf(40)); // 3

arr.forEach((val, i) => {
console.log(`${i}: ${val}`);
});

let sum = arr.reduce((acc, val) => acc + val, 0);
console.log(sum); // 150

// slice returns a NEW typed array (with its own buffer)
let sliced = arr.slice(1, 4);
console.log(sliced); // Uint8Array [20, 30, 40]

Methods that do not exist on typed arrays:

let arr = new Uint8Array([10, 20, 30]);

// ❌ These do NOT exist on typed arrays:
// arr.push(40); // TypeError: arr.push is not a function
// arr.pop(); // TypeError
// arr.shift(); // TypeError
// arr.unshift(5); // TypeError
// arr.splice(1, 1); // TypeError
// arr.concat(other); // TypeError

Typed arrays have a fixed length, so methods that would change the length (push, pop, splice, concat) are not available.

The set Method: Copying Data Between Typed Arrays​

The set method copies data from one array (or typed array) into another:

let dest = new Uint8Array(8);

// Copy from a regular array
dest.set([10, 20, 30]);
console.log(dest); // Uint8Array [10, 20, 30, 0, 0, 0, 0, 0]

// Copy from another typed array with an offset
let source = new Uint8Array([40, 50, 60]);
dest.set(source, 3); // Start copying at position 3
console.log(dest); // Uint8Array [10, 20, 30, 40, 50, 60, 0, 0]

The subarray Method: A View Without Copying​

Unlike slice (which copies data), subarray creates a new typed array view over the same buffer. Changes through the subarray affect the original:

let original = new Uint8Array([10, 20, 30, 40, 50]);

let sub = original.subarray(1, 4); // Elements 1, 2, 3 (NO COPY)
console.log(sub); // Uint8Array [20, 30, 40]

// Modifying the subarray modifies the original
sub[0] = 99;
console.log(original); // Uint8Array [10, 99, 30, 40, 50]

// They share the same buffer
console.log(sub.buffer === original.buffer); // true

Practical Example: Working with Image Pixel Data​

One of the most common uses of typed arrays in the browser is manipulating image pixels via the Canvas API:

let canvas = document.createElement("canvas");
canvas.width = 200;
canvas.height = 100;
let ctx = canvas.getContext("2d");

// Draw something
ctx.fillStyle = "blue";
ctx.fillRect(0, 0, 200, 100);

// Get the pixel data: returns a Uint8ClampedArray
let imageData = ctx.getImageData(0, 0, 200, 100);
let pixels = imageData.data; // Uint8ClampedArray

console.log(pixels.length); // 80000 (200 Γ— 100 Γ— 4 channels: RGBA)
console.log(pixels[0]); // 0 (Red channel of first pixel)
console.log(pixels[1]); // 0 (Green channel)
console.log(pixels[2]); // 255 (Blue channel)
console.log(pixels[3]); // 255 (Alpha channel)

// Invert all colors
for (let i = 0; i < pixels.length; i += 4) {
pixels[i] = 255 - pixels[i]; // Red
pixels[i + 1] = 255 - pixels[i + 1]; // Green
pixels[i + 2] = 255 - pixels[i + 2]; // Blue
// pixels[i + 3] stays the same (Alpha)
}

// Put the modified pixels back
ctx.putImageData(imageData, 0, 0);
document.body.append(canvas);

DataView: Flexible Binary Access​

While typed arrays are efficient for working with uniform data (arrays of the same type), DataView is designed for reading and writing mixed-format binary data where different bytes represent different types. It gives you explicit control over byte order (endianness) and lets you read any type from any byte offset.

DataView is the right tool for parsing binary file formats (PNG, ZIP, MP3, PDF), network protocol headers, and any binary structure where fields of different sizes and types are packed together.

Creating a DataView​

let buffer = new ArrayBuffer(16);
let view = new DataView(buffer);

// Or with offset and length
let partialView = new DataView(buffer, 4, 8); // Start at byte 4, span 8 bytes

Reading Data with Explicit Type and Offset​

DataView provides getter methods for every numeric type. Each method takes a byte offset and an optional endianness flag:

let buffer = new ArrayBuffer(16);
let view = new DataView(buffer);

// Write some values
view.setUint8(0, 255); // 1 byte at offset 0
view.setInt16(1, -1000, true); // 2 bytes at offset 1 (little-endian)
view.setUint32(3, 123456, true); // 4 bytes at offset 3 (little-endian)
view.setFloat64(7, 3.14159, true); // 8 bytes at offset 7 (little-endian)

// Read them back
console.log(view.getUint8(0)); // 255
console.log(view.getInt16(1, true)); // -1000
console.log(view.getUint32(3, true)); // 123456
console.log(view.getFloat64(7, true)); // 3.14159

DataView Methods Reference​

Getter methods (reading):

MethodBytesType
getInt8(offset)1Signed 8-bit integer
getUint8(offset)1Unsigned 8-bit integer
getInt16(offset, littleEndian?)2Signed 16-bit integer
getUint16(offset, littleEndian?)2Unsigned 16-bit integer
getInt32(offset, littleEndian?)4Signed 32-bit integer
getUint32(offset, littleEndian?)4Unsigned 32-bit integer
getFloat32(offset, littleEndian?)432-bit float
getFloat64(offset, littleEndian?)864-bit float
getBigInt64(offset, littleEndian?)8Signed 64-bit BigInt
getBigUint64(offset, littleEndian?)8Unsigned 64-bit BigInt

Setter methods (writing) follow the same pattern: setUint8(offset, value), setInt16(offset, value, littleEndian?), etc.

The littleEndian Parameter​

The second parameter (third for setters) controls byte order:

  • false or omitted: big-endian (most significant byte first, aka "network byte order")
  • true: little-endian (least significant byte first, native on most CPUs)
let buffer = new ArrayBuffer(4);
let view = new DataView(buffer);

// Write the number 0x01020304

// Big-endian (default): bytes stored as 01 02 03 04
view.setUint32(0, 0x01020304, false);
let bytes = new Uint8Array(buffer);
console.log(bytes); // Uint8Array [1, 2, 3, 4]

// Little-endian: bytes stored as 04 03 02 01
view.setUint32(0, 0x01020304, true);
console.log(bytes); // Uint8Array [4, 3, 2, 1]
tip

Binary file formats typically specify their endianness. Network protocols (TCP/IP) use big-endian ("network byte order"). Most CPU architectures (x86, ARM in standard mode) are little-endian. Always check the specification for the format you are working with and pass the correct littleEndian flag to DataView methods.

Practical Example: Parsing a Binary File Header​

Many binary file formats start with a header of mixed-type fields. Here is an example of parsing a simplified BMP-like image header:

// Simulated BMP-like header (14 bytes):
// Bytes 0-1: Magic number (Uint16, "BM" = 0x424D)
// Bytes 2-5: File size (Uint32, little-endian)
// Bytes 6-9: Reserved (Uint32)
// Bytes 10-13: Pixel data offset (Uint32, little-endian)

function parseBMPHeader(buffer) {
let view = new DataView(buffer);

let header = {
// Read 2-byte magic number as big-endian to preserve character order
magic: String.fromCharCode(view.getUint8(0), view.getUint8(1)),
// File size: 4 bytes, little-endian (BMP uses little-endian)
fileSize: view.getUint32(2, true),
// Reserved
reserved: view.getUint32(6, true),
// Pixel data offset
pixelOffset: view.getUint32(10, true)
};

return header;
}

// Create a simulated BMP header
let buffer = new ArrayBuffer(14);
let view = new DataView(buffer);

view.setUint8(0, 0x42); // 'B'
view.setUint8(1, 0x4D); // 'M'
view.setUint32(2, 1024000, true); // File size: ~1MB
view.setUint32(6, 0, true); // Reserved
view.setUint32(10, 54, true); // Pixel data starts at byte 54

let header = parseBMPHeader(buffer);
console.log(header);
// { magic: "BM", fileSize: 1024000, reserved: 0, pixelOffset: 54 }

Practical Example: Building a Binary Protocol Message​

// Custom protocol message:
// Byte 0: Message type (Uint8)
// Bytes 1-2: Sequence number (Uint16, big-endian)
// Bytes 3-6: Timestamp (Uint32, big-endian)
// Bytes 7-14: Payload value (Float64, big-endian)

function createMessage(type, sequence, timestamp, value) {
let buffer = new ArrayBuffer(15);
let view = new DataView(buffer);

view.setUint8(0, type);
view.setUint16(1, sequence, false); // big-endian (network byte order)
view.setUint32(3, timestamp, false);
view.setFloat64(7, value, false);

return buffer;
}

function parseMessage(buffer) {
let view = new DataView(buffer);

return {
type: view.getUint8(0),
sequence: view.getUint16(1, false),
timestamp: view.getUint32(3, false),
value: view.getFloat64(7, false)
};
}

// Create and parse a message
let msgBuffer = createMessage(1, 42, Date.now() / 1000 | 0, 98.6);
let parsed = parseMessage(msgBuffer);

console.log(parsed);
// { type: 1, sequence: 42, timestamp: 1710500000, value: 98.6 }

When to Use DataView vs. Typed Arrays​

ScenarioBest ChoiceWhy
Uniform data (all same type)Typed ArrayFaster, simpler indexing
Image pixels (RGBA)Uint8ClampedArrayClamping behavior, Canvas API
Audio samplesFloat32ArrayWeb Audio API uses it
Mixed-type binary structuresDataViewFlexible types at any offset
Network protocolsDataViewExplicit endianness control
Binary file parsingDataViewMixed fields, specific byte order
High-performance mathTyped ArrayArray-like access, optimized

Converting Between Binary and Text​

A common task is converting between binary data (ArrayBuffer/typed arrays) and text strings. Text in computers is ultimately stored as bytes according to an encoding (UTF-8, UTF-16, ASCII, etc.), so converting between the two requires specifying which encoding to use.

TextEncoder: String to Binary (UTF-8)​

TextEncoder converts a JavaScript string into a Uint8Array of UTF-8 bytes:

let encoder = new TextEncoder();  // Always encodes to UTF-8

let bytes = encoder.encode("Hello");
console.log(bytes); // Uint8Array [72, 101, 108, 108, 111]
console.log(bytes.length); // 5 (ASCII characters = 1 byte each in UTF-8)

// Multi-byte characters in UTF-8
let bytes2 = encoder.encode("CafΓ©");
console.log(bytes2); // Uint8Array [67, 97, 102, 195, 169]
console.log(bytes2.length); // 5 (Γ© takes 2 bytes in UTF-8)

// Emoji take 4 bytes each in UTF-8
let bytes3 = encoder.encode("Hi πŸ‘‹");
console.log(bytes3.length); // 7 (H=1, i=1, space=1, πŸ‘‹=4)

You can also encode into an existing buffer using encodeInto:

let encoder = new TextEncoder();
let buffer = new Uint8Array(20);

let result = encoder.encodeInto("Hello, World!", buffer);
console.log(result);
// { read: 13, written: 13 }

console.log(buffer.subarray(0, result.written));
// Uint8Array [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]

TextDecoder: Binary to String​

TextDecoder converts binary data (an ArrayBuffer, typed array, or DataView) back into a JavaScript string. It supports many encodings, not just UTF-8:

let decoder = new TextDecoder("utf-8"); // Default is UTF-8

let bytes = new Uint8Array([72, 101, 108, 108, 111]);
let text = decoder.decode(bytes);
console.log(text); // "Hello"

// Works with ArrayBuffer directly
let buffer = new ArrayBuffer(5);
new Uint8Array(buffer).set([87, 111, 114, 108, 100]);
console.log(decoder.decode(buffer)); // "World"

Other encodings:

// Windows-1251 (Cyrillic)
let win1251decoder = new TextDecoder("windows-1251");
let cyrillicBytes = new Uint8Array([207, 240, 232, 226, 229, 242]);
console.log(win1251decoder.decode(cyrillicBytes)); // "ΠŸΡ€ΠΈΠ²Π΅Ρ‚"

// ISO-8859-1 (Latin-1)
let latin1decoder = new TextDecoder("iso-8859-1");
let latinBytes = new Uint8Array([72, 101, 108, 108, 111, 33]);
console.log(latin1decoder.decode(latinBytes)); // "Hello!"

Handling Decoding Errors​

By default, TextDecoder replaces invalid byte sequences with the Unicode replacement character (U+FFFD, οΏ½). You can make it throw an error instead:

// Default: replace invalid bytes
let decoder = new TextDecoder("utf-8");
let invalidBytes = new Uint8Array([0xFF, 0xFE]);
console.log(decoder.decode(invalidBytes)); // "οΏ½οΏ½" (replacement characters)

// Strict mode: throw on invalid bytes
let strictDecoder = new TextDecoder("utf-8", { fatal: true });
try {
strictDecoder.decode(invalidBytes);
} catch (error) {
console.log(error.message); // "The encoded data was not valid."
}

Streaming Decoding​

When processing data in chunks (e.g., from a network stream), use the stream option to handle multi-byte characters that might be split across chunks:

let decoder = new TextDecoder("utf-8");

// "CafΓ©" in UTF-8: [67, 97, 102, 195, 169]
// If split across chunks: [67, 97, 102, 195] | [169]

let chunk1 = new Uint8Array([67, 97, 102, 195]);
let chunk2 = new Uint8Array([169]);

// With stream: true, the decoder keeps incomplete characters buffered
let part1 = decoder.decode(chunk1, { stream: true });
console.log(part1); // "Caf" (195 is incomplete, buffered for next chunk)

let part2 = decoder.decode(chunk2);
console.log(part2); // "Γ©" (completes the buffered character)

console.log(part1 + part2); // "CafΓ©"

Manual Hex and Base64 Conversions​

Sometimes you need to convert binary data to and from hexadecimal or base64 strings:

Hex conversion:

// Uint8Array β†’ Hex string
function toHex(uint8Array) {
return Array.from(uint8Array)
.map(byte => byte.toString(16).padStart(2, "0"))
.join("");
}

// Hex string β†’ Uint8Array
function fromHex(hexString) {
let bytes = new Uint8Array(hexString.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hexString.substr(i * 2, 2), 16);
}
return bytes;
}

let data = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]);
let hex = toHex(data);
console.log(hex); // "deadbeef"

let restored = fromHex(hex);
console.log(restored); // Uint8Array [222, 173, 190, 239]

Base64 conversion (browser):

// Uint8Array β†’ Base64
function toBase64(uint8Array) {
let binaryString = Array.from(uint8Array)
.map(byte => String.fromCharCode(byte))
.join("");
return btoa(binaryString);
}

// Base64 β†’ Uint8Array
function fromBase64(base64String) {
let binaryString = atob(base64String);
let bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}

let data = new Uint8Array([72, 101, 108, 108, 111]);
let base64 = toBase64(data);
console.log(base64); // "SGVsbG8="

let decoded = fromBase64(base64);
console.log(decoded); // Uint8Array [72, 101, 108, 108, 111]
console.log(new TextDecoder().decode(decoded)); // "Hello"

Converting Between Typed Arrays and Regular Arrays​

// Typed array β†’ Regular array
let typedArr = new Uint8Array([10, 20, 30, 40]);
let regularArr = Array.from(typedArr);
// or
let regularArr2 = [...typedArr];
console.log(regularArr); // [10, 20, 30, 40]
console.log(Array.isArray(regularArr)); // true

// Regular array β†’ Typed array
let arr = [100, 200, 300];
let typed = new Uint16Array(arr);
console.log(typed); // Uint16Array [100, 200, 300]

Practical Example: Binary File Analyzer​

Here is a comprehensive example that brings together ArrayBuffer, typed arrays, DataView, and text conversion to analyze binary files loaded through a file input:

<!DOCTYPE html>
<html>
<head>
<title>Binary File Analyzer</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; }
.hex-dump { font-family: monospace; font-size: 13px; background: #1e1e1e; color: #d4d4d4;
padding: 16px; border-radius: 8px; overflow-x: auto; white-space: pre; line-height: 1.6; }
.offset { color: #569cd6; }
.hex-byte { color: #ce9178; }
.ascii { color: #6a9955; }
.info { background: #f5f5f5; padding: 16px; border-radius: 8px; margin: 16px 0; }
.info dt { font-weight: bold; margin-top: 8px; }
.info dd { margin-left: 20px; font-family: monospace; }
</style>
</head>
<body>
<h1>Binary File Analyzer</h1>
<input type="file" id="file-input">
<div id="output"></div>

<script>
document.getElementById("file-input").addEventListener("change", async (event) => {
let file = event.target.files[0];
if (!file) return;

let arrayBuffer = await file.arrayBuffer();
let output = document.getElementById("output");
output.innerHTML = "";

// File info
let info = document.createElement("dl");
info.className = "info";
info.innerHTML = `
<dt>File Name</dt><dd>${file.name}</dd>
<dt>File Size</dt><dd>${arrayBuffer.byteLength.toLocaleString()} bytes</dd>
<dt>MIME Type</dt><dd>${file.type || "unknown"}</dd>
`;

// Detect file type from magic bytes
let magicType = detectFileType(arrayBuffer);
info.innerHTML += `<dt>Detected Type</dt><dd>${magicType}</dd>`;
output.append(info);

// Hex dump (first 256 bytes)
let hexDump = createHexDump(arrayBuffer, 256);
output.append(hexDump);
});

function detectFileType(buffer) {
let bytes = new Uint8Array(buffer, 0, Math.min(8, buffer.byteLength));

// Check magic bytes
if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4E && bytes[3] === 0x47) {
return "PNG Image";
}
if (bytes[0] === 0xFF && bytes[1] === 0xD8 && bytes[2] === 0xFF) {
return "JPEG Image";
}
if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46) {
return "GIF Image";
}
if (bytes[0] === 0x25 && bytes[1] === 0x50 && bytes[2] === 0x44 && bytes[3] === 0x46) {
return "PDF Document";
}
if (bytes[0] === 0x50 && bytes[1] === 0x4B && bytes[2] === 0x03 && bytes[3] === 0x04) {
return "ZIP Archive (or DOCX/XLSX/JAR)";
}
if (bytes[0] === 0x42 && bytes[1] === 0x4D) {
return "BMP Image";
}

// Check if it looks like text
let isText = bytes.every(b => (b >= 32 && b <= 126) || b === 9 || b === 10 || b === 13);
if (isText) return "Text File (ASCII)";

return "Unknown binary format";
}

function createHexDump(buffer, maxBytes) {
let bytes = new Uint8Array(buffer, 0, Math.min(maxBytes, buffer.byteLength));
let pre = document.createElement("pre");
pre.className = "hex-dump";

let lines = [];
for (let offset = 0; offset < bytes.length; offset += 16) {
let chunk = bytes.subarray(offset, Math.min(offset + 16, bytes.length));

// Offset column
let offsetStr = `<span class="offset">${offset.toString(16).padStart(8, "0")}</span>`;

// Hex bytes
let hexParts = [];
for (let i = 0; i < 16; i++) {
if (i < chunk.length) {
hexParts.push(`<span class="hex-byte">${chunk[i].toString(16).padStart(2, "0")}</span>`);
} else {
hexParts.push(" ");
}
}
let hexStr = hexParts.join(" ");

// ASCII representation
let asciiParts = [];
for (let i = 0; i < chunk.length; i++) {
let byte = chunk[i];
let char = (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : ".";
// Escape HTML special characters
if (char === "<") char = "&lt;";
if (char === ">") char = "&gt;";
if (char === "&") char = "&amp;";
asciiParts.push(char);
}
let asciiStr = `<span class="ascii">${asciiParts.join("")}</span>`;

lines.push(`${offsetStr} ${hexStr} ${asciiStr}`);
}

if (buffer.byteLength > maxBytes) {
lines.push(`\n... ${(buffer.byteLength - maxBytes).toLocaleString()} more bytes ...`);
}

pre.innerHTML = lines.join("\n");
return pre;
}
</script>
</body>
</html>

This example demonstrates:

  • file.arrayBuffer() to get binary data from a file input
  • Uint8Array to read individual bytes for magic number detection and hex display
  • subarray for efficient byte range access without copying
  • Byte-to-hex conversion with toString(16)
  • Byte-to-ASCII conversion with String.fromCharCode
  • Practical binary format detection by reading header bytes

Summary​

JavaScript's binary data system is built on three layers: the raw storage (ArrayBuffer), the views that interpret the storage (typed arrays and DataView), and the utilities that convert between binary and text.

ArrayBuffer:

  • A fixed-length block of raw bytes, initialized to zero.
  • Cannot be read or written directly. Requires a view.
  • slice() creates a copy of a portion. byteLength gives the size.

Typed Arrays (Uint8Array, Int32Array, Float64Array, etc.):

  • Views over an ArrayBuffer that interpret bytes as a uniform numeric type.
  • Support index-based access (arr[0]) and most array methods (map, filter, sort, find, reduce).
  • Do not support length-changing methods (push, pop, splice, concat).
  • Overflow wraps around (modular arithmetic), except Uint8ClampedArray which clamps.
  • set() copies data in. subarray() creates a view without copying. slice() copies.
  • Multiple typed arrays can share the same underlying buffer.
  • Use the system's native endianness for multi-byte values.

DataView:

  • A flexible view that reads/writes individual values of any type at any byte offset.
  • Explicit endianness control (littleEndian parameter on every get/set call).
  • Best for mixed-format binary data: file headers, network protocols, structured binary messages.
  • Methods: getUint8, setUint8, getInt16, setInt16, getUint32, setUint32, getFloat32, setFloat32, getFloat64, setFloat64, plus BigInt variants.

Text Conversion:

  • TextEncoder converts strings to Uint8Array (UTF-8).
  • TextDecoder converts binary data back to strings (supports UTF-8, Windows-1251, ISO-8859-1, and many more).
  • Use { stream: true } for chunked decoding of multi-byte characters.
  • Use { fatal: true } to throw on invalid byte sequences instead of replacing with οΏ½.