buf.toString()
Decodes a Buffer, or a byte range of it, into a string using the encoding you name.
buf.toString([encoding[, start[, end]]])- Added in Node.js v0.1.90
Parameters
| Parameter | Type | Description |
|---|---|---|
$encodingoptional | string= 'utf8' | The character encoding to decode to. |
$startoptional | integer= 0 | The byte offset to start decoding from. |
$endoptional | integer= buf.length | The byte offset to stop decoding at (not inclusive). |
Return value
stringThe decoded contents of buf between start and end.
Errors
Throws ERR_UNKNOWN_ENCODING if encoding is not a supported encoding name. Out-of-range start and end values are clamped rather than throwing.
buf.toString() turns bytes into text. The same bytes produce completely different strings depending on the encoding you ask for, so the argument is the whole decision.
The encodings you will use
| Encoding | Use it for |
|---|---|
'utf8' (default) | Text. Invalid sequences become U+FFFD rather than throwing. |
'hex' | Digests, IDs, debugging. Two characters per byte. |
'base64' / 'base64url' | Putting binary in JSON, a URL or a header. |
'latin1' | Byte-for-character round trips with no re-encoding. |
'utf16le' | Windows-native and some database text. |
start and end are byte offsets
They are not character offsets. Cutting in the middle of a multi-byte character yields a replacement character, which is why decoding stream chunks individually corrupts non-ASCII text at chunk boundaries — use string_decoder's StringDecoder there, or concatenate first and decode once.
Watch the size
Decoding materialises the whole range as a JavaScript string, roughly doubling memory for ASCII. On a large file, decode the slice you need rather than the entire buffer, and note that strings are capped at buffer.constants.MAX_STRING_LENGTH.
Example
When to use it
- Print a hash or random ID as hex.
- Encode a small binary payload as base64 to put inside JSON.
- Decode a file read with fs.readFile into text.
- Inspect just the first few bytes of a buffer to sniff a file format.
Frequently asked questions
How do I convert a Buffer to a string in Node.js?
Call buf.toString(), which decodes as UTF-8 by default. Pass an encoding such as 'hex' or 'base64' as the first argument when you want a different representation.
Why does my Buffer.toString output show question marks or diamonds?
Those are U+FFFD replacement characters, produced when the bytes are not valid in the encoding you asked for. It usually means a multi-byte character was cut across a chunk or slice boundary, or the data is not UTF-8 at all.
What is the difference between base64 and base64url in Node?
base64url uses - and _ instead of + and / and omits the trailing = padding, so the result is safe in URLs, filenames and cookies. Both decode back to identical bytes with Buffer.from.
More examples
hex and base64url in practice
hex is the conventional way to display a digest and is exactly twice the byte length. base64url produces a shorter, URL-safe string than base64 for the same bytes, and the final lines show decoding a three-byte range instead of the whole buffer to identify a file type.
const crypto = require('node:crypto');
const digest = crypto.createHash('sha256').update('hello').digest();
console.log(digest.toString('hex'));
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
// base64url is safe in URLs and cookies: no + / = characters.
const token = crypto.randomBytes(16);
console.log(token.toString('base64url').length); // 22
console.log(token.toString('base64').length); // 24
// Only the first bytes are needed to sniff a PNG.
const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
console.log(png.toString('hex', 1, 4) === '504e47'); // trueWhy decoding chunk by chunk corrupts text
toString has no memory between calls, so a multi-byte character straddling two chunks is destroyed in both halves. StringDecoder buffers the trailing partial sequence until the next write completes it, which is what stream.setEncoding uses internally.
const { StringDecoder } = require('node:string_decoder');
const bytes = Buffer.from('café'); // 5 bytes; é spans bytes 3 and 4
const first = bytes.subarray(0, 4);
const second = bytes.subarray(4);
// Naive: each chunk decoded on its own.
console.log(JSON.stringify(first.toString() + second.toString()));
// "caf��" - the character was split and both halves became U+FFFD
// Correct: a decoder that holds the incomplete sequence back.
const decoder = new StringDecoder('utf8');
console.log(JSON.stringify(decoder.write(first))); // "caf"
console.log(JSON.stringify(decoder.write(second))); // "é"Detecting invalid UTF-8 instead of silently replacing it
toString treats malformed input as recoverable and substitutes replacement characters, which quietly corrupts data you meant to reject. TextDecoder with fatal:true surfaces the problem, and latin1 is the escape hatch when you need a lossless byte-to-string mapping.
const invalid = Buffer.from([0x61, 0xff, 0x62]);
console.log(JSON.stringify(invalid.toString('utf8')));
// "a�b" - no error raised
try {
new TextDecoder('utf-8', { fatal: true }).decode(invalid);
} catch (err) {
console.log(err.name, '-', err.message);
// TypeError - The encoded data was not valid for encoding utf-8
}
// latin1 round-trips any byte sequence without loss.
console.log(Buffer.from(invalid.toString('latin1'), 'latin1').equals(invalid)); // true
Discussion