ArrayBuffer
ArrayBuffer is a built-in object that represents a raw binary data buffer. It represents a fixed-length sequence of bytes that can be used to store and manipulate binary data in JavaScript. Unlike regular JavaScript arrays, ArrayBuffer stores raw binary data without any type interpretation.
Creating an ArrayBuffer
Create an ArrayBuffer using the new keyword with the ArrayBuffer constructor:
const buffer = new ArrayBuffer(16);
// ArrayBuffer { byteLength: 16 }
The constructor allocates exactly the number of bytes you request, all initialized to zero. The memory is contiguous, so the buffer can be passed directly to Web APIs like WebGL, Web Audio, and file readers that expect raw binary data. Once created, the buffer’s size cannot change — you need to allocate a new buffer and copy data if you need more space.
The constructor takes a single parameter:
length: The number of bytes to allocate (must be a non-negative integer)
If the requested byte length exceeds reasonable limits or is negative, a RangeError is thrown:
new ArrayBuffer(-1); // RangeError: Invalid array length
new ArrayBuffer(Infinity); // RangeError: Invalid array length
Passing a negative number or a value that exceeds available memory throws a RangeError. The maximum practical size depends on the engine and available system memory — most engines cap allocations at 2^31 - 1 bytes (about 2 GB). Always wrap buffer creation in a try/catch when the size comes from user input or an external source.
ArrayBuffer Properties
byteLength
The byteLength property returns the size of the ArrayBuffer in bytes:
const buffer = new ArrayBuffer(64);
buffer.byteLength; // 64
byteLength is set at construction time and never changes — it is a read-only accessor, not a writable property. Attempting to assign to it is silently ignored in non-strict mode. This immutability is what makes ArrayBuffer safe to pass between different parts of a program: no code can unexpectedly resize it.
This property is read-only and cannot be changed after the buffer is created.
ArrayBuffer Methods
slice()
Creates a shallow copy of a portion of the buffer into a new ArrayBuffer:
const buffer = new ArrayBuffer(8);
// Fill with some data
const view = new Uint8Array(buffer);
view[0] = 1;
view[1] = 2;
view[2] = 3;
view[3] = 4;
// Slice from byte 1 to 4 (exclusive)
const sliced = buffer.slice(1, 4);
sliced.byteLength; // 3
new Uint8Array(sliced); // Uint8Array [2, 3, 4]
slice() copies the bytes, so changes to the original buffer do not affect the slice and vice versa. The copy is shallow — it duplicates the raw bytes, not any objects that views built on the buffer might reference. Use slice() when you need to hand a subset of the buffer to another function without exposing the full data.
Syntax: buffer.slice(start, end)
start: Start index (inclusive)end: End index (exclusive)
ArrayBuffer.isView()
A static method that returns true if the argument is a view on an ArrayBuffer:
const buffer = new ArrayBuffer(8);
ArrayBuffer.isView(buffer); // false
ArrayBuffer.isView(new Uint8Array(buffer)); // true
ArrayBuffer.isView(new DataView(buffer)); // true
ArrayBuffer.isView(new Int32Array(buffer, 0, 2)); // true
ArrayBuffer.isView() checks for any typed array or DataView instance — it does not check whether the view is currently backed by a valid (non-detached) buffer. A view on a detached buffer still returns true. This method is a type guard, not a validity check.
This is useful for type-checking before performing buffer operations.
ArrayBuffer.transfer() (ES2024)
Transfers ownership of an ArrayBuffer’s backing store to a new ArrayBuffer:
const original = new ArrayBuffer(16);
// Fill with some data
new Uint8Array(original).set([1, 2, 3, 4]);
const transferred = ArrayBuffer.transfer(original, 8);
original.byteLength; // 0 (original is detached)
transferred.byteLength; // 8
new Uint8Array(transferred); // Uint8Array [1, 2, 3, 4]
transfer() moves ownership of the backing store from the source buffer to a new one. The source becomes detached — its byteLength drops to zero and any views on it can no longer read or write. This is a zero-copy operation: the raw bytes stay at the same memory location, only the ownership changes hands.
Syntax: ArrayBuffer.transfer(sourceBuffer, newLength)
sourceBuffer: The ArrayBuffer to transfernewLength: The length of the new buffer in bytes
The original buffer becomes detached (byteLength becomes 0). If newLength is greater than the original length, the new bytes are zero-initialized.
ArrayBuffer.transferToFixedLength() (ES2024)
Similar to transfer() but always creates a fixed-length ArrayBuffer:
const resizable = new ArrayBuffer(16, { maxLength: 32 });
new Uint8Array(resizable).set([10, 20, 30]);
const fixed = ArrayBuffer.transferToFixedLength(resizable, 8);
resizable.byteLength; // 0 (detached)
fixed.byteLength; // 8
new Uint8Array(fixed); // Uint8Array [10, 20, 30, 0, 0, 0, 0, 0]
Key difference: transfer() may return a resizable buffer if the source was resizable, while transferToFixedLength() always returns a fixed-length buffer.
Working with ArrayBuffer
ArrayBuffer by itself doesn’t provide a way to read or write data. You need to use typed arrays or DataView to interact with the buffer’s contents.
Using TypedArrays
TypedArrays provide typed views into ArrayBuffer:
const buffer = new ArrayBuffer(16);
// Create different views on the same buffer
const uint8 = new Uint8Array(buffer);
const int32 = new Int32Array(buffer);
const float64 = new Float64Array(buffer);
// Write using one view
uint8[0] = 255;
uint8[1] = 255;
uint8[2] = 0;
uint8[3] = 0;
// Read using another view
int32[0]; // 65280 (0x0000FF in little-endian)
float64[0]; // 2.83...e-319
Different typed arrays interpret the same bytes differently:
Int8Array: 8-bit signed integerUint8Array: 8-bit unsigned integerInt16Array: 16-bit signed integerUint16Array: 16-bit unsigned integerInt32Array: 32-bit signed integerUint32Array: 32-bit unsigned integerFloat32Array: 32-bit floating pointFloat64Array: 64-bit floating point
Using DataView
DataView provides low-level access to read/write different data types at specific offsets:
const buffer = new ArrayBuffer(12);
const dataView = new DataView(buffer);
// Write different types at specific offsets
dataView.setInt8(0, 42);
dataView.setUint16(2, 1000);
dataView.setFloat64(4, 3.14159);
// Read them back
dataView.getInt8(0); // 42
dataView.getUint16(2); // 1000
dataView.getFloat64(4); // 3.14159
DataView is useful when you need to work with mixed data types in a single buffer.
Detached Buffers
An ArrayBuffer can become “detached” when ownership of its backing store is transferred. Once detached, the buffer’s byteLength becomes 0 and no views on it can be used:
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
view[0] = 1;
// Detach by transferring
const newBuffer = ArrayBuffer.transfer(buffer, 8);
view[0]; // 0 (view is now detached)
buffer.byteLength; // 0
Detaching is permanent — a detached buffer cannot be reattached or revived. The only safe pattern after transferring is to stop using the original buffer and any views on it. This is why transfer() and transferToFixedLength() return the new buffer: the returned buffer is the one you should keep.
Sharing memory between workers
ArrayBuffer is the foundation for sharing memory between Web Workers:
// In main thread
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedView = new Int32Array(sharedBuffer);
// Post the buffer to a worker
worker.postMessage({ sharedBuffer }, [sharedBuffer]);
// In worker
self.onmessage = (e) => {
const sharedView = new Int32Array(e.data.sharedBuffer);
// Both threads can now read/write directly
};
SharedArrayBuffer enables true shared memory between threads. Unlike regular postMessage, which copies data, an ArrayBuffer transferred in the transfer list moves ownership without copying — the sending thread loses access, and the receiving thread gains it. For bidirectional access, use SharedArrayBuffer with Atomics for safe concurrent reads and writes.
Practical Examples
Reading binary file data
// Simulating reading a binary header
function readHeader(buffer) {
const view = new DataView(buffer);
return {
magic: view.getUint32(0),
version: view.getUint16(4),
flags: view.getUint16(6),
dataSize: view.getUint32(8)
};
}
const headerBuffer = new ArrayBuffer(12);
const headerView = new DataView(headerBuffer);
headerView.setUint32(0, 0x4A534F4E); // "JSON" in ASCII
headerView.setUint16(4, 1); // version 1
headerView.setUint16(6, 0); // flags
headerView.setUint32(8, 1024); // data size
readHeader(headerBuffer);
// { magic: 1212766542, version: 1, flags: 0, dataSize: 1024 }
DataView is the right tool for parsing binary headers because it lets you read multi-byte values at arbitrary offsets without caring about alignment. The getUint32 and getUint16 methods default to big-endian byte order; pass true as the second argument for little-endian. This pattern is common when working with file formats, network protocols, and WebSocket binary frames.
Buffer reuse with pooling
class BufferPool {
constructor(size = 1024) {
this.size = size;
this.pool = [];
}
acquire() {
if (this.pool.length > 0) {
return this.pool.pop();
}
return new ArrayBuffer(this.size);
}
release(buffer) {
if (buffer.byteLength === this.size) {
this.pool.push(buffer);
}
}
}
const pool = new BufferPool(256);
const buf1 = pool.acquire();
// ... use buf1 ...
pool.release(buf1);
Browser Compatibility
| Method | Support |
|---|---|
| ArrayBuffer | ES6 (all modern browsers) |
| slice() | ES6 |
| isView() | ES6 |
| transfer() | ES2024 (limited) |
| transferToFixedLength() | ES2024 (limited) |
The ES2024 methods have limited browser support at the time of writing.
See Also
- TypedArrays — Typed array views for ArrayBuffer
- DataView — Mixed-type binary data access
- BigInt — Arbitrary-precision integers