buf.subarray()
Returns a Buffer that views part of the original, sharing its memory rather than copying it.
buf.subarray([start[, end]])- Added in Node.js v3.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$startoptional | integer= 0 | Where the new Buffer will start. |
$endoptional | integer= buf.length | Where 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
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.
// 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
// worldslice 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.
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()); // abcRetaining 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.
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