buf.subarray()

Returns a Buffer that views part of the original, sharing its memory rather than copying it.

Signature
buf.subarray([start[, end]])
  • Added in Node.js v3.0.0

Parameters

ParameterTypeDescription
$startoptionalinteger= 0Where the new Buffer will start.
$endoptionalinteger= buf.lengthWhere the new Buffer will end (not inclusive).

Return value

BufferA new Buffer that references the same memory as the original, offset and cropped by start and end.

Errors

Does not throw. Out-of-range indexes are clamped to the buffer's bounds, so an impossible range yields a zero-length Buffer.

Changelog

  • PHP v17.5.0buf.slice(), the older alias with identical behaviour, was deprecated in favour of buf.subarray().

buf.subarray() creates a second Buffer object pointing at the same bytes. Nothing is copied, so it costs almost nothing — and writing through either view is visible through the other.

The behaviour that surprises people

Because the memory is shared, view[0] = 0x68 changes the parent buffer too. That is deliberate and is what makes zero-copy parsing possible, but it means you must not hand a subarray to code that might mutate it unless the parent is expendable. To detach, copy: Buffer.from(buf.subarray(a, b)).

Negative and out-of-range indexes

Negative values count back from the end, so buf.subarray(-4) is the last four bytes. Indexes outside the buffer are clamped rather than throwing, and a start past end yields a zero-length buffer.

subarray versus slice

buf.slice() is the deprecated alias with the same sharing semantics. It was deprecated because it collides with Uint8Array.prototype.slice(), which copies — identical spelling, opposite behaviour. Use subarray to share and Uint8Array.prototype.slice.call(buf, a, b), or simply Buffer.from(buf.subarray(a, b)), to copy.

One more consequence: a small subarray keeps the entire parent allocation alive, so slicing 10 bytes out of a 50 MB read and caching them retains all 50 MB.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Split a received frame into header and payload without copying.
  • Return only the bytes you actually wrote from a reusable read buffer.
  • Walk a length-prefixed stream field by field in a parser.
  • Trim a fixed-size block down to its used prefix before writing it out.

Frequently asked questions

What is the difference between buf.subarray and buf.slice in Node.js?

They behave identically — both return a view sharing the parent's memory — but buf.slice is deprecated. It was deprecated because Uint8Array.prototype.slice copies instead of sharing, making the same method name mean opposite things.

Does buf.subarray copy the data?

No. It returns a new Buffer object pointing at the same memory, so writing through the view changes the original. Wrap it in Buffer.from() when you need an independent copy.

How do I get the last N bytes of a Buffer?

Use buf.subarray(-n), since negative indexes count back from the end. Out-of-range values are clamped, so asking for more bytes than the buffer holds returns the whole buffer rather than throwing.

More examples

Zero-copy frame parsing

Each yielded frame is a window onto the original buffer, so parsing a megabyte of traffic allocates only the small Buffer objects, not the bytes. The consumer must copy anything it intends to keep past the lifetime of the wire buffer.

Example · javascript
// Wire format: 4-byte big-endian length, then that many bytes of payload.
function* frames(buf) {
  let offset = 0;
  while (offset + 4 <= buf.length) {
    const len = buf.readUInt32BE(offset);
    if (offset + 4 + len > buf.length) return;      // incomplete tail
    yield buf.subarray(offset + 4, offset + 4 + len);
    offset += 4 + len;
  }
}

const wire = Buffer.concat([
  Buffer.from([0, 0, 0, 2]), Buffer.from('hi'),
  Buffer.from([0, 0, 0, 5]), Buffer.from('world')
]);

for (const frame of frames(wire)) console.log(frame.toString());
// hi
// world

slice copies for Uint8Array but not for Buffer

This is exactly why buf.slice was deprecated: the same method name means share on a Buffer and copy on its superclass. Note that borrowing Uint8Array's slice returns a Uint8Array, not a Buffer, so Buffer.from around a subarray is usually the better copy.

Example · javascript
const buf = Buffer.from('abcdef');

const shared = buf.subarray(0, 3);
shared[0] = 0x5a;
console.log(buf.toString());        // Zbcdef - parent changed

buf[0] = 0x61;                      // reset

// Uint8Array.prototype.slice always copies.
const copied = Uint8Array.prototype.slice.call(buf, 0, 3);
copied[0] = 0x5a;
console.log(buf.toString());        // abcdef - parent untouched
console.log(Buffer.isBuffer(copied));  // false, it is a Uint8Array

// The clearest way to get an independent Buffer:
const independent = Buffer.from(buf.subarray(0, 3));
console.log(independent.toString()); // abc

Retaining a subarray retains the whole parent

A subarray holds a reference to the parent's ArrayBuffer, so garbage collection cannot reclaim the other 50 MB while the 16-byte view is cached. Printing .buffer.byteLength makes the retained size visible; copying on the way into the cache is the fix.

Example · javascript
const cache = new Map();

function cacheIdBad(bigBuffer) {
  cache.set('id', bigBuffer.subarray(0, 16));   // pins all of bigBuffer
}

function cacheIdGood(bigBuffer) {
  cache.set('id', Buffer.from(bigBuffer.subarray(0, 16)));  // 16 bytes retained
}

const big = Buffer.alloc(50 * 1024 * 1024, 1);
cacheIdBad(big);
console.log(cache.get('id').buffer.byteLength);   // 52428800

cacheIdGood(big);
console.log(cache.get('id').buffer.byteLength);   // 16

Discussion

  • Be the first to comment on this lesson.