Buffers & Binary Data
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Node.js 26 (26.10.0, released 2026-09-21, the Current line that becomes LTS on 2026-10-28), with the differences
of Node.js 24 "Krypton" (24.21.0, Active LTS) and Node.js 22 "Jod" (22.23.3, Maintenance) called out; versions and stability levels were
checked against the official documentation on 2026-10-03, and every example below was run on all three lines unless the text says
otherwise. JavaScript strings are sequences of UTF-16 code units; files, sockets, hashes, images and protocol frames are sequences of
bytes. Everything that crosses that boundary in Node.js goes through one of a few classes, and this page is the map: Buffer (Stability 2,
Stable) is Node.js’s byte container and a subclass of Uint8Array; ArrayBuffer, the typed arrays and DataView are the language-level
building blocks Buffer sits on; node:string_decoder (Stability 2, Stable) decodes byte streams without splitting characters;
TextEncoder and TextDecoder are the WHATWG Encoding Standard classes, available as globals; and Blob and File are the immutable,
shareable byte containers of the web platform. For the JavaScript side of typed arrays read
JavaScript Standard Library Utilities; this page owns what Node.js adds. The
running scenario is the Bookshelf application that grows across this section, described in
Introduction & Architecture; here it parses a binary record format and sniffs cover
images. Streams deliver Buffer chunks, so Streams and The File System
are the natural next reads.
The stability and version map
| API | Status (Node.js 26 docs) | Version notes |
|---|---|---|
|
2 Stable |
|
|
Stable |
An |
|
Stable |
Default raised from 8192 to 65536 in v26.3.0 and v24.18.0, so it is still 8192 on Node.js 22 (measured: 8192 on 22.23.3, 65536 on 24.21.0 and 26.10.0). |
|
0 Deprecated (DEP0005) |
Application deprecation (code outside |
|
0 Deprecated (DEP0158) |
Documentation-only deprecation since v17.5.0 and v16.15.0; use |
|
0 Deprecated |
Use |
|
Removed (DEP0030, End-of-Life) |
Documentation-only in v6.0.0, runtime deprecation in v24.0.0, removed in v25.0.0. It is still exported on Node.js 24 and 22. |
|
3 Legacy |
Use |
|
Stable |
|
|
ECMAScript feature of V8, not a Node.js API |
Not documented by Node.js. Observed: present on Node.js 26.10.0 (V8 14.6), absent on 24.21.0 (V8 13.6) and 22.23.3 (V8 12.4). |
|
2 Stable |
Each invalid character is replaced by a single replacement character since v8.0.0. |
|
Globals (also |
Global since v11.0.0. The supported encodings depend on the ICU configuration of the build. |
|
Stable |
No longer experimental since v18.0.0 and v16.17.0; |
|
Stable |
No longer experimental since v20.0.0; instances became cloneable in v23.0.0. |
|
Stable |
Stable since v24.0.0 and v22.17.0; |
What a Buffer is
A Buffer is a fixed-length sequence of bytes. It cannot grow or shrink; to make one longer you allocate a new one and copy. It is
implemented as a subclass of Uint8Array, so every typed-array method works on it and every Node.js API that accepts a
Buffer also accepts a plain Uint8Array. The largest allowed size is exposed
as buffer.constants.MAX_LENGTH (also buffer.kMaxLength); on 64-bit builds it is Number.MAX_SAFE_INTEGER since v22.0.0, so memory,
not the constant, is the real limit. buffer.constants.MAX_STRING_LENGTH is the largest string, counted in UTF-16 code units (536870888 on
the Node.js versions used for this page).
The mental model that prevents most bugs is that a Buffer is a view onto an ArrayBuffer`: it has a `buffer (the underlying
ArrayBuffer), a byteOffset into it and a length. Some creation methods copy bytes and some share them; the sections below mark which.
Creating buffers: from, alloc, allocUnsafe, concat
Three families of factory methods replaced the old constructor. Buffer.from() copies (or wraps) existing data, Buffer.alloc() returns
zero-filled memory, and Buffer.allocUnsafe() returns memory that may contain old bytes and is therefore only for code that overwrites every
byte before reading any. Buffer.concat(list[, totalLength]) copies several buffers into a new one and truncates or zero-pads to
totalLength when it is given. Buffer.byteLength(string, encoding) tells you how many bytes a string needs without allocating.
import { Buffer } from 'node:buffer';
const fromString = Buffer.from('Café ☕'); // UTF-8 by default
const fromArray = Buffer.from([0x42, 0x4f, 0x4f, 0x4b]);
const zeroed = Buffer.alloc(8); // always zero-filled
const filled = Buffer.alloc(8, 'ab'); // repeated fill pattern
const scratch = Buffer.allocUnsafe(8).fill(0); // uninitialised until overwritten
console.log(fromString, fromString.length, fromString.toString().length);
console.log(Buffer.byteLength('Café ☕'), Buffer.byteLength('Café ☕', 'utf16le'));
console.log(fromArray.toString('latin1'));
console.log(zeroed, filled.toString(), scratch);
const parts = [Buffer.from('Boo'), Buffer.from('k'), Buffer.from('shelf')];
const joined = Buffer.concat(parts); // copies; total length computed
console.log(joined.toString(), Buffer.concat(parts, 4).toString());
console.log(Buffer.isBuffer(joined), Buffer.isBuffer(new Uint8Array(2)), joined instanceof Uint8Array);
<Buffer 43 61 66 c3 a9 20 e2 98 95> 9 6
9 12
BOOK
<Buffer 00 00 00 00 00 00 00 00> abababab <Buffer 00 00 00 00 00 00 00 00>
Bookshelf Book
true false true
Source: Node.js API: Buffer
The string 'Café ☕' has six characters but nine UTF-8 bytes: length of a Buffer is always bytes, never characters. The reference table
below lists the creation API.
| Method | Copies or shares | Notes |
|---|---|---|
|
Copies |
Each value is truncated with |
|
Copies |
Encoding defaults to |
|
Copies |
Also accepts a |
|
Shares |
Writes through the |
|
Copies |
Copies the bytes of a typed array or |
|
New zeroed memory |
Slower than |
|
New uninitialised memory |
May use the shared pool when |
|
New uninitialised memory |
Never uses the shared pool: its own |
|
Copies |
Pass |
The pool, and why allocUnsafe is "unsafe"
To make small allocations fast, Buffer.allocUnsafe(), Buffer.from(string), Buffer.concat() and Buffer.from(array) may carve a slice
out of a shared, pre-allocated block (the pool, Buffer.poolSize bytes) when the requested size is at most half the pool size.
Buffer.allocUnsafeSlow() never does. Two consequences matter in practice. First, memory from allocUnsafe() is uninitialised, so reading it
before overwriting every byte can leak whatever the process had in that memory earlier, including secrets from another request. Second, a
pooled Buffer shares its ArrayBuffer with unrelated buffers, so buf.buffer is bigger than buf and buf.byteOffset is usually not 0;
never pass buf.buffer to code that expects exactly your bytes, and keep a small slice of a pooled block alive only if you accept pinning the
whole block. A long-lived small buffer is the use case for allocUnsafeSlow().
The command-line option --zero-fill-buffers makes every newly allocated Buffer zero-filled, including the unsafe ones. It exists to
enforce safety in code you cannot audit and has a measurable performance cost, so it is a mitigation, not a default. Measured on all three
lines:
node --zero-fill-buffers -p "Buffer.allocUnsafe(5)"
# <Buffer 00 00 00 00 00>
Encodings
Converting between strings and bytes always names an encoding. The default is UTF-8. Names are case-insensitive. The supported encodings are:
| Encoding | Kind | Notes |
|---|---|---|
|
Text |
Default. Invalid byte sequences decode to the replacement character U+FFFD. |
|
Text |
Little-endian only; 2 or 4 bytes per character. |
|
Text |
One byte per character for U+0000 to U+00FF; other characters are truncated into that range. |
|
Legacy text |
7-bit data. When decoding it clears the high bit of every byte. Prefer |
|
Binary-to-text |
Accepts the URL-safe alphabet and ignores whitespace when decoding. |
|
Binary-to-text |
Added in v15.7.0 and v14.18.0. Omits padding when encoding; accepts regular base64 when decoding. |
|
Binary-to-text |
Two characters per byte. Decoding stops silently at the first invalid pair or at a trailing odd digit. |
For the binary-to-text encodings the naming is reversed: turning a Buffer into a base64 string is "encoding" and the reverse is "decoding".
Buffer.isEncoding(name) tells you whether a name is supported.
import { Buffer } from 'node:buffer';
const buf = Buffer.from('hello world', 'utf8');
console.log(buf.toString('hex')); // 68656c6c6f20776f726c64
console.log(buf.toString('base64')); // aGVsbG8gd29ybGQ=
console.log(Buffer.from([0xfb, 0xff, 0xfe]).toString('base64')); // + and / alphabet, padded
console.log(Buffer.from([0xfb, 0xff, 0xfe]).toString('base64url')); // - and _ alphabet, no padding
console.log(Buffer.from('-__-', 'base64url'), Buffer.from('+//+', 'base64url'));
console.log(Buffer.from('1ag123', 'hex'), Buffer.from('1a7', 'hex'), Buffer.from('1634', 'hex'));
console.log(Buffer.from('fhqw', 'utf16le'), Buffer.from('é', 'latin1'), Buffer.from('€', 'latin1'));
console.log(Buffer.from([0xe2, 0x82]).toString(), Buffer.from([0xff]).toString().codePointAt(0).toString(16));
console.log(Buffer.isEncoding('UTF-8'), Buffer.isEncoding('utf/8'), Buffer.isEncoding('binary'));
68656c6c6f20776f726c64
aGVsbG8gd29ybGQ=
+//+
-__-
<Buffer fb ff fe> <Buffer fb ff fe>
<Buffer 1a> <Buffer 1a> <Buffer 16 34>
<Buffer 66 00 68 00 71 00 77 00> <Buffer e9> <Buffer ac>
� fffd
true false true
Source: Node.js API: Buffer
Read the output line by line. Malformed hex is truncated, not rejected ('1ag123' gives one byte), so validate hex input before decoding it.
'latin1' silently destroys characters outside U+0000 to U+00FF (the euro sign became the byte ac). A truncated UTF-8 sequence decodes to
the replacement character U+FFFD, which is lossy: you cannot get the original bytes back from the string. Never convert binary data to a
string with toString() (UTF-8) and back; use 'base64', 'hex' or keep the Buffer.
Slicing: subarray shares, copies copy
buf.subarray([start[, end]]) returns a new Buffer that references the same memory, offset and cropped; negative indexes count from the
end and an end past the length is clamped. It is inherited from TypedArray.prototype.subarray(). The older buf.slice() behaves the same
on Buffer but is deprecated (DEP0158, documentation-only) because it is incompatible with Uint8Array.prototype.slice(), which copies; the
same name meaning "view" on one class and "copy" on its superclass was a bug magnet. To copy, use Buffer.from(buffer), buf.copy(target, …)
or Buffer.concat([buf]).
import { Buffer } from 'node:buffer';
const original = Buffer.from('bookshelf');
const view = original.subarray(0, 4); // no copy: shares memory
const copy = Buffer.from(original.subarray(4)); // Buffer.from(buffer) copies
view[0] = 0x42; // 'B'
console.log(original.toString(), view.toString(), copy.toString());
copy[0] = 0x53; // does not touch original
console.log(original.toString(), copy.toString());
console.log(original.subarray(-5).toString(), original.subarray(2, 100).toString());
// Small Buffers share one pooled ArrayBuffer, so byteOffset is usually not 0.
const small = Buffer.from('abc');
console.log(small.byteOffset > 0, small.buffer.byteLength > small.length, Buffer.poolSize);
const exact = Buffer.allocUnsafeSlow(3);
console.log(exact.byteOffset, exact.buffer.byteLength); // 0 3: its own ArrayBuffer
Bookshelf Book shelf
Bookshelf Shelf
shelf okshelf
true true 65536
0 3
Source: Node.js API: Buffer
The first line shows that writing through view changed original (Bookshelf), while copy stayed independent. The value byteOffset > 0
is the usual case for pooled buffers but not guaranteed (a buffer can start the pool), which is why the example prints a comparison and not
an exact offset.
Reading and writing binary structures
Buffer has a method per integer and float width and endianness: readUInt8(), readUInt16LE(), readUInt16BE(), readUInt32LE(),
readBigUInt64BE(), readFloatLE(), readDoubleBE() and the matching write…() methods, plus readUIntBE(offset, byteLength) and
readIntLE(offset, byteLength) for 1 to 6 byte values. Every write…() method returns the offset just past what it wrote, so calls chain
naturally; buf.write(string, offset, length, encoding) returns the number of bytes written. Endianness is in the method name, which makes
mistakes visible in review: network protocols are usually big-endian (BE), most CPUs you run on are little-endian (LE). Reading outside the
buffer throws a RangeError with code ERR_OUT_OF_RANGE instead of returning garbage. The search and comparison helpers are
indexOf(), lastIndexOf(), includes() (all accept a number, a string with an encoding, or a Buffer), equals(), compare() and
Buffer.compare(), fill(), copy(), swap16(), swap32(), swap64() and toJSON().
The example is a small, original binary header for a Bookshelf catalogue record: a four-byte magic number, a version, a flags byte, a big-endian year, a little-endian price in cents and a fixed 16-byte title.
import { Buffer } from 'node:buffer';
// A tiny binary record: magic "BKSH", version, flags, year, price in cents, 16-byte title.
const record = Buffer.alloc(4 + 1 + 1 + 2 + 4 + 16);
let offset = 0;
offset += record.write('BKSH', offset, 'ascii');
offset = record.writeUInt8(1, offset); // write* returns the next offset
offset = record.writeUInt8(0b101, offset);
offset = record.writeUInt16BE(2024, offset);
offset = record.writeUInt32LE(1999, offset); // endianness is in the method name
record.write('Efficient Node.js', offset, 16, 'utf8');
console.log(record.toString('hex'));
console.log(record.toString('ascii', 0, 4), record.readUInt8(4), record.readUInt16BE(6), record.readUInt32LE(8));
console.log(record.toString('utf8', 12).replace(/\0+$/, ''));
console.log(record.readUInt8(4), record.subarray(0, 4).equals(Buffer.from('BKSH')));
console.log(Buffer.compare(Buffer.from('a'), Buffer.from('b')), record.indexOf('BKSH'), record.includes(0x07));
try { record.readUInt32LE(record.length - 2); } catch (e) { console.log(e.code); }
424b5348010507e8cf070000456666696369656e74204e6f64652e6a
BKSH 1 2024 1999
Efficient Node.j
1 true
-1 0 true
ERR_OUT_OF_RANGE
Source: Node.js API: Buffer
Notice that the 17-character title was cut to 16 bytes (Efficient Node.j): write() never writes a partial multi-byte character, but it does
truncate, so size fixed fields for the longest value you accept, or reject longer values explicitly. For anything beyond a handful of fields,
a DataView (below) or a small parser class is easier to keep correct than offsets scattered through the code.
ArrayBuffer, TypedArrays and DataView
ArrayBuffer is a raw, fixed-length block of memory with no way to read it. A typed array (Uint8Array, Int16Array, Uint32Array,
Float32Array, BigInt64Array, and so on) is a view that interprets a range of an ArrayBuffer as numbers of one type, using the platform’s
byte order (little-endian on every mainstream Node.js platform). A DataView is a view that reads and writes any type at any byte offset with an
explicit endianness argument (big-endian by default), which is why it is the right tool for protocol and file formats. SharedArrayBuffer is the
variant that can be shared between threads; it is covered in Worker Threads. Buffer fits in as a
Uint8Array subclass with extra methods, so the conversions between the worlds are cheap and mostly about whether memory is shared.
import { Buffer } from 'node:buffer';
const ab = new ArrayBuffer(8);
const bytes = new Uint8Array(ab);
const asBuffer = Buffer.from(ab, 2, 4); // shares memory with ab, bytes 2..5
asBuffer.fill(0xff);
console.log(bytes); // changes visible through the Uint8Array
const f32 = new Float32Array([1.5, -2]);
const copy = Buffer.copyBytesFrom(f32); // copies the bytes of the view
const alias = Buffer.from(f32.buffer, f32.byteOffset, f32.byteLength); // aliases them
f32[0] = 3;
console.log(copy.readFloatLE(0), alias.readFloatLE(0));
const dv = new DataView(new ArrayBuffer(4));
dv.setUint32(0, 0x01020304); // DataView is big-endian by default
console.log(new Uint8Array(dv.buffer), new Uint8Array(new Uint32Array([0x01020304]).buffer));
console.log(Buffer.from('hi').toJSON());
const u8 = new Uint8Array([104, 105]);
console.log(Buffer.from(u8.buffer, u8.byteOffset, u8.length).toString()); // wrap, do not copy
console.log(Object.getPrototypeOf(Buffer.prototype) === Uint8Array.prototype);
Uint8Array(8) [
0, 0, 255, 255,
255, 255, 0, 0
]
1.5 3
Uint8Array(4) [ 1, 2, 3, 4 ] Uint8Array(4) [ 4, 3, 2, 1 ]
{ type: 'Buffer', data: [ 104, 105 ] }
hi
true
Source: Node.js API: Buffer, MDN: TypedArray, MDN: DataView
The output shows the rule in action: asBuffer wrote bytes 2 to 5 of the shared ArrayBuffer; copy kept the old float while alias followed
the change (1.5 versus 3); and the same number 0x01020304 is laid out big-endian by DataView and little-endian by Uint32Array on this
machine. Buffer.from(typedArray) is not the way to get the bytes of a Float32Array: it copies the element values, each truncated to a byte
(observed: Buffer.from(new Float32Array([1.5, 300])) is <Buffer 01 2c>). Use Buffer.copyBytesFrom() (copy) or Buffer.from(view.buffer, view.byteOffset, view.byteLength) (alias).
Base64 and hex on plain Uint8Array
A recent ECMAScript addition gives Uint8Array its own conversions: toBase64(), toHex(), Uint8Array.fromBase64(), Uint8Array.fromHex()
and the in-place setFromBase64() and setFromHex(), with options for the 'base64' or 'base64url' alphabet and for omitting padding.
These come from V8, not from Node.js, and are not part of the Node.js API documentation, so treat what follows as observed: they exist on
Node.js 26.10.0 (V8 14.6) and are missing on 24.21.0 (V8 13.6) and 22.23.3 (V8 12.4). Feature-detect and fall back to Buffer, which
runs everywhere and is what Node.js-only code should keep using. The helpers matter for code that must also run in browsers and
other runtimes, where Buffer does not exist.
const bytes = new Uint8Array([0xfb, 0xff, 0xfe, 0x00]);
if (typeof bytes.toBase64 === 'function') {
console.log(bytes.toBase64());
console.log(bytes.toBase64({ alphabet: 'base64url', omitPadding: true }));
console.log(bytes.toHex());
console.log(Uint8Array.fromBase64('+//+AA=='), Uint8Array.fromHex('68656c6c6f'));
const target = new Uint8Array(8);
console.log(target.setFromHex('cafe'), target);
} else {
console.log('no native Uint8Array base64/hex helpers; use Buffer');
const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
console.log(buf.toString('base64'), buf.toString('base64url'), buf.toString('hex'));
}
+//+AA==
-__-AA
fbfffe00
Uint8Array(4) [ 251, 255, 254, 0 ] Uint8Array(5) [ 104, 101, 108, 108, 111 ]
{ read: 4, written: 2 } Uint8Array(8) [
202, 254, 0, 0,
0, 0, 0, 0
]
no native Uint8Array base64/hex helpers; use Buffer
+//+AA== -__-AA fbfffe00
Source: Node.js API: Buffer (Buffers and TypedArrays, base64/base64url/hex encodings), MDN: Uint8Array.prototype.toBase64(), MDN: Uint8Array.fromBase64(), TC39: Uint8Array to/from base64 proposal
Unlike Buffer.from(hex, 'hex'), which silently truncates malformed input, the native fromHex() and fromBase64() throw on invalid input (observed on Node.js 26.10.0: Uint8Array.fromHex('1ag1') and
Uint8Array.fromBase64('@@') both throw a SyntaxError).
Buffer methods on a plain Uint8Array
The Buffer documentation states that all methods on the Buffer prototype are callable with a Uint8Array instance, which is handy for library
code that receives either. This is documented from the Node.js 24 documentation onward and is absent from the 22.x documentation.
const { toString, write } = Buffer.prototype;
const u8 = new Uint8Array(5);
write.call(u8, 'hello', 0, 5, 'utf8'); // Buffer methods applied to a plain Uint8Array
console.log(toString.call(u8, 'utf8'), toString.call(u8, 'hex'));
hello 68656c6c6f
Source: Node.js API: Buffer
Observed on Node.js 22.23.3: the same code throws TypeError: this.utf8Write is not a function, so on the 22 line wrap the view with
Buffer.from(u8.buffer, u8.byteOffset, u8.length) first.
SlowBuffer and the deprecated constructor
Two older ways of getting a buffer are worth recognising in existing code. SlowBuffer was a class that returned an unpooled buffer; it was
deprecated in documentation in v6.0.0, produced a runtime warning from v24.0.0 and was removed in v25.0.0 (DEP0030, End-of-Life). Its
replacement is Buffer.allocUnsafeSlow(size); a codemod, npx codemod@latest @nodejs/slow-buffer-to-buffer-alloc-unsafe-slow, performs the
migration. The Buffer() function and new Buffer() constructor (DEP0005) chose its behaviour from the type of its first argument, so an
attacker who could make a JSON field a number instead of a string made the server allocate memory of that size; before Node.js 8.0.0 the
buffer was also uninitialised, which exposed process memory. It still works, but code outside node_modules prints a deprecation
warning. Use Buffer.from(), Buffer.alloc() or Buffer.allocUnsafe() and validate the type of the argument yourself.
const buffer = require('node:buffer');
console.log('SlowBuffer export:', typeof buffer.SlowBuffer);
if (buffer.SlowBuffer) console.log(new buffer.SlowBuffer(4).length); // 24/22: warns (DEP0030)
console.log(Buffer.allocUnsafeSlow(4).length); // the replacement
const legacy = new Buffer(10); // DEP0005 warning
console.log(legacy.length);
# Node.js 26
SlowBuffer export: undefined
4
10
(node:PID) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
# Node.js 24
SlowBuffer export: function
4
4
10
(node:PID) [DEP0030] DeprecationWarning: SlowBuffer() is deprecated. Please use Buffer.allocUnsafeSlow()
(node:PID) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
# Node.js 22
SlowBuffer export: function
4
4
10
(node:PID) [DEP0005] DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the Buffer.alloc(), Buffer.allocUnsafe(), or Buffer.from() methods instead.
The warning text was observed on the three lines (the example uses CommonJS because the new Buffer() warning is the point and it is the
form legacy code takes). The Node.js 22 line has no runtime warning for SlowBuffer, consistent with the version history above.
StringDecoder: decoding a stream without splitting characters
Bytes arrive in chunks whose boundaries have nothing to do with character boundaries. A UTF-8 character is one to four bytes, so a chunk can
end in the middle of one; calling chunk.toString() on each chunk and concatenating produces replacement characters. StringDecoder
(node:string_decoder) keeps the incomplete trailing bytes in an internal buffer: write(chunk) returns the longest decodable prefix and holds
the rest for the next call, and end([chunk]) flushes whatever remains, replacing an incomplete sequence with a substitution character. It
supports 'utf8' (the default), 'utf16le', 'base64', 'base64url', 'hex', 'latin1' and 'ascii' (observed: 'binary' and 'ucs2' are accepted as aliases of 'latin1' and 'utf16le'). A decoder can be reused after end().
import { StringDecoder } from 'node:string_decoder';
import { Buffer } from 'node:buffer';
const bytes = Buffer.from('Écrire un café ☕ à Paris');
const chunks = [bytes.subarray(0, 1), bytes.subarray(1, 14), bytes.subarray(14, 16), bytes.subarray(16)];
console.log(chunks.map((c) => c.toString()).join('')); // broken: splits multi-byte characters
const decoder = new StringDecoder('utf8');
let text = '';
for (const chunk of chunks) text += decoder.write(chunk);
text += decoder.end();
console.log(text);
const d2 = new StringDecoder('utf8');
console.log(JSON.stringify(d2.write(Buffer.from([0xe2, 0x82]))), JSON.stringify(d2.end()));
const d3 = new StringDecoder('base64');
console.log(d3.write(Buffer.from('ab')), d3.end());
��crire un café ☕ à Paris
Écrire un café ☕ à Paris
"" "�"
YWI=
Source: Node.js API: String decoder
You rarely construct a StringDecoder yourself: a readable stream configured with an encoding (readable.setEncoding('utf8') or the
encoding option of fs.createReadStream()) uses one internally. The next example shows the two correct ways to read text in small chunks,
and a second Bookshelf task: sniffing a file type from its first bytes without reading the whole file.
import { Buffer } from 'node:buffer';
import { createReadStream } from 'node:fs';
import { open, writeFile, rm } from 'node:fs/promises';
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
// Sniff a cover image by reading only its first bytes into a fixed-size Buffer.
async function isPng(path) {
const handle = await open(path, 'r');
try {
const head = Buffer.alloc(PNG_SIGNATURE.length);
const { bytesRead } = await handle.read(head, 0, head.length, 0);
return bytesRead === head.length && head.equals(PNG_SIGNATURE);
} finally {
await handle.close();
}
}
await writeFile('cover.bin', Buffer.concat([PNG_SIGNATURE, Buffer.from('rest of the image')]));
await writeFile('notes.txt', 'Écrire des café ☕ à Paris, 日本語');
console.log(await isPng('cover.bin'), await isPng('notes.txt'));
// Collect a stream: keep chunks as Buffers and decode once at the end.
const chunks = [];
for await (const chunk of createReadStream('notes.txt', { highWaterMark: 5 })) chunks.push(chunk);
console.log(chunks.length, Buffer.concat(chunks).toString());
// Or let the stream decode for you: the encoding option uses a StringDecoder internally.
let text = '';
for await (const piece of createReadStream('notes.txt', { highWaterMark: 5, encoding: 'utf8' })) text += piece;
console.log(text);
await rm('cover.bin');
await rm('notes.txt');
true false
9 Écrire des café ☕ à Paris, 日本語
Écrire des café ☕ à Paris, 日本語
Collecting Buffer chunks and calling Buffer.concat(chunks) once, then decoding, is also the right shape for binary data; for large inputs
do not collect at all and process chunks as they arrive, as described in Streams.
TextEncoder and TextDecoder
TextEncoder and TextDecoder implement the WHATWG Encoding Standard, the same classes browsers provide. They are globals, and also exported
by node:util. They are the portable choice for code shared with browsers, and the only choice for encodings other than the ones Buffer
knows. TextEncoder only produces UTF-8 (its encoding property is always 'utf-8'): encode(string) returns a Uint8Array, and
encodeInto(source, destination) writes into an existing array and reports read (UTF-16 code units consumed) and written (bytes
produced), stopping before it would split a character. TextDecoder takes an encoding label (default 'utf-8') and options: fatal: true
throws a TypeError with code ERR_ENCODING_INVALID_ENCODED_DATA on invalid input instead of inserting replacement characters, and
ignoreBOM: true keeps a leading byte order mark in the output instead of stripping it. decode(input, { stream: true }) keeps incomplete
trailing bytes for the next call, like StringDecoder. With full ICU data (the default in official builds), TextDecoder supports the WHATWG
encoding list, including windows-1252, iso-8859-*, shift_jis, gbk, big5, euc-kr and utf-16le; builds with small-icu or without
Intl support support fewer, which util.html tabulates.
const encoder = new TextEncoder();
const dest = new Uint8Array(6);
console.log(encoder.encode('€uro'), encoder.encodeInto('€uro', dest), dest);
const decoder = new TextDecoder();
console.log(decoder.decode(new Uint8Array([0xe2, 0x82, 0xac])));
console.log(JSON.stringify(decoder.decode(new Uint8Array([0xff]))));
try { new TextDecoder('utf-8', { fatal: true }).decode(new Uint8Array([0xff])); }
catch (e) { console.log(e.name, e.code); }
const bom = new Uint8Array([0xef, 0xbb, 0xbf, 0x41]);
console.log(new TextDecoder().decode(bom).length, new TextDecoder('utf-8', { ignoreBOM: true }).decode(bom).length);
console.log(new TextDecoder('windows-1252').decode(new Uint8Array([0x80, 0xe9])), new TextDecoder('utf-16le').decode(new Uint8Array([0x41, 0, 0x3d, 0xd8, 0x00, 0xde])));
const streaming = new TextDecoder();
console.log(streaming.decode(new Uint8Array([0xe2, 0x82]), { stream: true }) + streaming.decode(new Uint8Array([0xac])));
console.log(decoder.encoding, encoder.encoding);
Uint8Array(6) [ 226, 130, 172, 117, 114, 111 ] { read: 4, written: 6 } Uint8Array(6) [ 226, 130, 172, 117, 114, 111 ]
€
"�"
TypeError ERR_ENCODING_INVALID_ENCODED_DATA
1 2
€é A😀
€
utf-8 utf-8
The first output line is wrapped by console.log printing the same destination array twice: encode() returned a new array, and
encodeInto() filled dest (read: 4 code units, written: 6 bytes, because the euro sign takes three). The BOM line shows the default
stripping the mark (length 1) and ignoreBOM: true keeping it (length 2). The byte 0x80 is the euro sign in windows-1252, which is not
what latin1 (ISO-8859-1) means in Buffer; in the WHATWG standard the labels latin1 and iso-8859-1 are in fact aliases of
windows-1252, a difference worth knowing when an HTML or HTTP header names a "latin1" charset.
| Need | Prefer |
|---|---|
UTF-8, UTF-16LE, Latin-1, hex, base64 in Node.js-only code |
|
Code shared with browsers and other runtimes |
|
Legacy encodings ( |
|
Error on invalid input |
|
Decode a chunked stream |
|
Blob and File
Blob is the web platform’s immutable chunk of raw bytes with a MIME type. It is created from an array of strings, ArrayBuffer`s, typed
arrays, `Buffer`s or other blobs, and is global (also exported from `node:buffer). You read it asynchronously: blob.text(),
blob.arrayBuffer(), blob.bytes() (a Uint8Array, added in v22.3.0 and v20.16.0) or blob.stream() (a web ReadableStream), and cut it
with blob.slice(start, end, type), which does not copy the data. Because a Blob is immutable it can be posted to worker threads without
copying; see Worker Threads. File extends Blob with a name and a lastModified timestamp, and
is what FormData entries carry. fs.openAsBlob(path) returns a Blob backed lazily by a file on disk, so a large file can be passed to
fetch() or a worker without being read into memory; the file must not change after the blob is created, or reading fails with a
DOMException. Node.js 26.10.0 added a synchronous fs.openAsBlobSync(). A Blob is not a stream and not a Buffer: you cannot read it
synchronously, so convert it once at an API boundary and work with bytes beyond it.
import { Blob, File } from 'node:buffer';
import { openAsBlob } from 'node:fs';
import { writeFile, rm } from 'node:fs/promises';
const blob = new Blob(['Hello, ', new Uint8Array([0x42, 0x6f, 0x6f, 0x6b]), '!'], { type: 'text/plain' });
console.log(blob.size, blob.type, await blob.text());
console.log(await blob.bytes(), new Uint8Array(await blob.arrayBuffer()).length);
const part = blob.slice(7, 11, 'text/plain');
console.log(await part.text());
const file = new File([blob], 'greeting.txt', { type: 'text/plain', lastModified: 0 });
console.log(file.name, file.size, file.lastModified, file instanceof Blob);
let total = 0;
for await (const chunk of blob.stream()) total += chunk.byteLength;
console.log(total);
await writeFile('sample.bin', Buffer.from('on disk'));
const onDisk = await openAsBlob('sample.bin'); // lazily backed by the file
console.log(onDisk.size, await onDisk.text());
await rm('sample.bin');
console.log(globalThis.Blob === Blob, typeof globalThis.File);
12 text/plain Hello, Book!
Uint8Array(12) [
72, 101, 108, 108, 111,
44, 32, 66, 111, 111,
107, 33
] 12
Book
greeting.txt 12 0 true
12
7 on disk
true function
Other node:buffer helpers
The module has a few small functions that save hand-written validation. buffer.isUtf8(input) and buffer.isAscii(input) report whether a
Buffer, ArrayBuffer or typed array holds valid UTF-8 or only 7-bit ASCII, which is cheaper and safer than decoding and looking for
replacement characters. buffer.transcode(source, fromEnc, toEnc) converts between 'utf8', 'ucs2'/'utf16le', 'latin1',
'ascii' and 'binary', substituting a replacement character for what the target cannot represent. buffer.INSPECT_MAX_BYTES controls how many bytes console.log() prints for a buffer (50 by
default). buffer.resolveObjectURL(id) returns the Blob for a blob:nodedata: URL created with URL.createObjectURL() (stable since v24.0.0
and v22.17.0). The legacy buffer.atob() and buffer.btoa() exist for browser compatibility; use Buffer.from(data, 'base64') and
buf.toString('base64') in new code.
import buffer, { Buffer, isUtf8, isAscii, transcode, constants } from 'node:buffer';
console.log(isUtf8(Buffer.from('café')), isUtf8(Buffer.from([0xff])), isAscii(Buffer.from('cafe')), isAscii(Buffer.from('café')));
console.log(transcode(Buffer.from('€'), 'utf8', 'utf16le'), constants.MAX_LENGTH, constants.MAX_STRING_LENGTH);
console.log(typeof buffer.SlowBuffer, typeof buffer.atob, typeof Buffer.allocUnsafeSlow);
console.log(Buffer.from('aGk=', 'base64').toString(), Buffer.from('hi').toString('base64'));
function instead of undefined for SlowBuffer)true false true false
<Buffer ac 20> 9007199254740991 536870888
undefined function function
hi aGk=
Source: Node.js API: Buffer
Pitfalls
| Mistake | Fix |
|---|---|
Using |
Use |
Treating |
It is bytes. Use |
Concatenating string chunks ( |
Collect |
Converting binary data through |
The conversion is lossy. Use |
Passing |
A pooled buffer shares a larger |
Expecting |
It does not on |
Using |
It copies element values, truncated to bytes. Use |
Accepting a size from untrusted input and allocating it |
Validate and cap it. A large allocation is a denial of service, and |
Comparing secrets with |
Use a constant-time comparison; see Crypto, Passwords & Secrets. |
Trusting hex or base64 decoding to reject bad input |
|
What changed recently
| Version | Change |
|---|---|
v26.10.0 |
|
v26.8.0 |
|
v26.3.0 and v24.18.0 |
|
v25.0.0 |
|
v24.0.0 and v22.17.0 |
|
v22.3.0 |
|
v22.0.0 |
|
Where the books are outdated, narrower or silent
Samer Buna’s Efficient Node.js (O’Reilly, 2025) explains streams at length but treats Buffer and typed arrays only in passing, so the
material on this page (the pool, subarray() sharing, DataView, TextDecoder, Blob) has to come from the official documentation.
Jonathan Wexler’s Node.js Projects (O’Reilly, 2025) uses Buffer.from(JSON.stringify(…)) to turn queue payloads into bytes and
toString('hex') to print random salt bytes; both are correct current idioms, but the book’s queue and password examples are covered by the
pages on messaging and cryptography because their problems are not about buffers. Neither book mentions that SlowBuffer is gone (removed in
v25.0.0), that buf.slice() is deprecated, that the pool size changed in v26.3.0 and v24.18.0, or the Web-standard TextDecoder, Blob and
File; the current approach in each case is the section above.
Related pages
-
Streams for chunked, back-pressured processing of
Bufferdata. -
The File System for
readFile,FileHandleandopenAsBlob. -
Crypto, Passwords & Secrets for hashes, random bytes and constant-time comparison.
-
Worker Threads for transferring `ArrayBuffer`s and sharing `Blob`s.
-
Globals & Web Platform APIs for the other globals Node.js shares with browsers.
-
Don’t Block the Event Loop for the cost of large synchronous conversions.