Skip to main content

Buffer Operations: Reading, Writing, and Manipulating Binary Data

Once you've created a Buffer, you need to know how to work with it—reading bytes, writing data, copying sections, comparing buffers, and transforming data. Node.js provides a comprehensive set of methods for these operations, from simple byte access to complex multi-byte integer reads.

Think of Buffer operations like working with a specialized array where each element is a byte (0-255), but with additional superpowers for handling binary data patterns, different number formats, and efficient memory operations.

What You Need to Know First

Required reading (in order):

  1. Buffer Basics: Working with Binary Data in Node.js - Understanding Buffer creation and fundamentals
  2. Buffer Memory Management: Allocation, Pools, and Performance - How buffers are allocated

Helpful background:

  • Basic understanding of binary and hexadecimal numbers
  • Familiarity with JavaScript array operations (helpful but not required)

What We'll Cover in This Article

By the end of this guide, you'll master:

  • Direct byte access and modification
  • Reading and writing strings with different encodings
  • Buffer slicing and creating views
  • Copying data between buffers
  • Comparing and searching buffers
  • Filling buffers with patterns
  • Working with multi-byte integers
  • Buffer concatenation and manipulation

What We'll Explain Along the Way

These concepts will be explained with examples:

  • The difference between views and copies (critical concept)
  • Little-endian vs big-endian byte order (with visual diagrams)
  • Integer types (signed vs unsigned, 8-bit, 16-bit, 32-bit)
  • Buffer immutability vs mutability
  • Index boundaries and safety

Basic Buffer Access: Reading and Writing Bytes

Direct Byte Access

Buffers behave like arrays where each element is a byte (0-255):

// Create a buffer with some data
const buffer = Buffer.from([72, 101, 108, 108, 111]); // "Hello"

// Read individual bytes
console.log(buffer[0]); // 72 ('H')
console.log(buffer[1]); // 101 ('e')
console.log(buffer[4]); // 111 ('o')

// Write individual bytes
buffer[0] = 74; // Change 'H' (72) to 'J' (74)
console.log(buffer.toString()); // "Jello"

// Check buffer length
console.log(buffer.length); // 5 bytes

// Access last byte
console.log(buffer[buffer.length - 1]); // 111 ('o')

Visual Representation:

Buffer: [72, 101, 108, 108, 111]
Index: 0 1 2 3 4
ASCII: H e l l o

After buffer[0] = 74:

Buffer: [74, 101, 108, 108, 111]
Index: 0 1 2 3 4
ASCII: J e l l o

Bounds Checking and Safety

const buffer = Buffer.from([1, 2, 3, 4, 5]);

// ✅ Reading within bounds
console.log(buffer[0]); // 1
console.log(buffer[4]); // 5

// ⚠️ Reading outside bounds (returns undefined)
console.log(buffer[5]); // undefined
console.log(buffer[10]); // undefined
console.log(buffer[-1]); // undefined

// ⚠️ Writing outside bounds (silently ignored)
buffer[10] = 99; // No error, but does nothing
console.log(buffer.length); // Still 5 (not expanded)
console.log(buffer[10]); // undefined

// Why?
// Buffers have FIXED size
// You cannot grow or shrink them
// Out-of-bounds operations are safe but ineffective

Checking Bounds Before Access:

function safeReadByte(buffer: Buffer, index: number): number | undefined {
// Check if index is valid
if (index < 0 || index >= buffer.length) {
console.log(`Index ${index} out of bounds (length: ${buffer.length})`);
return undefined;
}
return buffer[index];
}

const buf = Buffer.from([10, 20, 30]);
console.log(safeReadByte(buf, 1)); // 20
console.log(safeReadByte(buf, 5)); // undefined (with warning)

Iterating Over Buffer Bytes

const buffer = Buffer.from([72, 101, 108, 108, 111]);

// Method 1: Traditional for loop
console.log("Method 1 - for loop:");
for (let i = 0; i < buffer.length; i++) {
console.log(
`buffer[${i}] = ${buffer[i]} ('${String.fromCharCode(buffer[i])}')`
);
}

// Method 2: for...of loop
console.log("\nMethod 2 - for...of:");
for (const byte of buffer) {
console.log(`Byte: ${byte} ('${String.fromCharCode(byte)}')`);
}

// Method 3: forEach (Buffer extends Uint8Array)
console.log("\nMethod 3 - forEach:");
buffer.forEach((byte, index) => {
console.log(`buffer[${index}] = ${byte}`);
});

// Method 4: Array methods (map, filter, reduce)
console.log("\nMethod 4 - Array methods:");
const doubled = Array.from(buffer).map((byte) => byte * 2);
console.log(doubled); // [144, 202, 216, 216, 222]

Buffer Properties and Length

buffer.length vs buffer.byteLength

const buffer = Buffer.from([1, 2, 3, 4, 5]);

// buffer.length: Number of bytes in the buffer
console.log(buffer.length); // 5

// buffer.byteLength: Same as buffer.length (for compatibility)
console.log(buffer.byteLength); // 5

// They're always equal for Buffers
console.log(buffer.length === buffer.byteLength); // true

// Note: buffer.buffer.byteLength is different (underlying ArrayBuffer size)
console.log(buffer.buffer.byteLength); // 8192 (if from pool)

String Length vs Buffer Length:

// ASCII string: 1 character = 1 byte
const ascii = "Hello";
const asciiBuffer = Buffer.from(ascii);
console.log(ascii.length); // 5 characters
console.log(asciiBuffer.length); // 5 bytes

// UTF-8 string: Multi-byte characters
const utf8 = "café"; // 'é' is 2 bytes in UTF-8
const utf8Buffer = Buffer.from(utf8);
console.log(utf8.length); // 4 characters
console.log(utf8Buffer.length); // 5 bytes

// Emoji string: Even more bytes
const emoji = "Hello 👋"; // Wave emoji is 4 bytes
const emojiBuffer = Buffer.from(emoji);
console.log(emoji.length); // 8 (JavaScript counts UTF-16 code units)
console.log(emojiBuffer.length); // 10 bytes in UTF-8

// Lesson: NEVER assume string.length === buffer.length

String Operations: Reading and Writing Text

buffer.toString() - Convert Buffer to String

const buffer = Buffer.from([72, 101, 108, 108, 111]);

// Default encoding: UTF-8
console.log(buffer.toString()); // "Hello"

// Specify encoding explicitly
console.log(buffer.toString("utf8")); // "Hello"

// Convert to hex representation
console.log(buffer.toString("hex")); // "48656c6c6f"

// Convert to base64
console.log(buffer.toString("base64")); // "SGVsbG8="

// Convert to binary string
console.log(buffer.toString("binary")); // "Hello"

Partial String Conversion:

const buffer = Buffer.from("Hello World");

// Extract portion of buffer as string
console.log(buffer.toString("utf8", 0, 5)); // "Hello"
console.log(buffer.toString("utf8", 6, 11)); // "World"
console.log(buffer.toString("utf8", 6)); // "World" (to end)

// Breakdown:
// toString(encoding, start, end)
// start: Starting byte index (inclusive)
// end: Ending byte index (exclusive)

// Visual:
// Buffer: H e l l o W o r l d
// Index: 0 1 2 3 4 5 6 7 8 9 10
// └─────────┘ └──────────┘
// "Hello" "World"

Different Encodings:

// Create buffer with multi-byte characters
const text = "café";
const buffer = Buffer.from(text, "utf8");

console.log(buffer); // <Buffer 63 61 66 c3 a9>
console.log(buffer.length); // 5 bytes

// Convert back with different encodings
console.log(buffer.toString("utf8")); // "café" (correct)
console.log(buffer.toString("ascii")); // "café" (wrong! ASCII can't handle 0xC3 0xA9)
console.log(buffer.toString("latin1")); // "café" (wrong! Latin-1 treats bytes individually)
console.log(buffer.toString("hex")); // "636166c3a9" (hex representation)

// Lesson: Always use the same encoding for encode/decode

buffer.write() - Write String to Buffer

// Create an empty buffer
const buffer = Buffer.alloc(20);

// Write a string
const bytesWritten = buffer.write("Hello");
console.log(`Wrote ${bytesWritten} bytes`); // 5
console.log(buffer.toString("utf8", 0, bytesWritten)); // "Hello"
console.log(buffer); // <Buffer 48 65 6c 6c 6f 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00>

// Write with offset
buffer.write("World", 6); // Start at byte 6
console.log(buffer.toString("utf8", 0, 11)); // "Hello World"

// Write with length limit
const buf2 = Buffer.alloc(10);
buf2.write("Hello World", 0, 5); // Only write 5 bytes
console.log(buf2.toString()); // "Hello" (truncated)

Write with Different Encodings:

const buffer = Buffer.alloc(20);

// Write UTF-8 (default)
buffer.write("café", 0, "utf8");
console.log(buffer.slice(0, 5)); // <Buffer 63 61 66 c3 a9>

// Write hex string
buffer.write("48656c6c6f", 0, "hex");
console.log(buffer.toString("utf8", 0, 5)); // "Hello"

// Write base64
buffer.write("SGVsbG8=", 0, "base64");
console.log(buffer.toString("utf8", 0, 5)); // "Hello"

// Full signature:
// buffer.write(string, offset, length, encoding)

Handling Write Overflow:

const buffer = Buffer.alloc(5);

// Try to write more than buffer can hold
const result = buffer.write("Hello World");

console.log(`Bytes written: ${result}`); // 5 (only what fits)
console.log(buffer.toString()); // "Hello" (rest truncated)

// Safe writing pattern:
function safeWrite(buffer: Buffer, text: string, offset = 0): boolean {
const bytesNeeded = Buffer.byteLength(text);
const spaceAvailable = buffer.length - offset;

if (bytesNeeded > spaceAvailable) {
console.log(
`⚠️ Not enough space! Need ${bytesNeeded}, have ${spaceAvailable}`
);
return false;
}

buffer.write(text, offset);
return true;
}

const buf = Buffer.alloc(10);
safeWrite(buf, "Hi", 0); // ✓ Success
safeWrite(buf, "Hello World", 0); // ✗ Failure (warning shown)

Buffer Slicing and Views

buffer.slice() - Create View (Not a Copy!)

Critical Concept: slice() creates a view over the original buffer, not a copy. Changes to the slice affect the original!

const original = Buffer.from("Hello World");

// Create a slice (view)
const slice = original.slice(0, 5); // "Hello"

console.log(slice.toString()); // "Hello"

// Modify the slice
slice[0] = 74; // Change 'H' (72) to 'J' (74)

// Original is also modified!
console.log(slice.toString()); // "Jello"
console.log(original.toString()); // "Jello World" (changed!)

// Why? slice() creates a VIEW, not a copy
console.log(slice.buffer === original.buffer); // true (same underlying memory)

Visual Explanation:

Original Buffer:
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│H│e│l│l│o│ │W│o│r│l│d│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
0 1 2 3 4 5 6 7 8 9 10

Slice (view of bytes 0-4):
┌─┬─┬─┬─┬─┐
│H│e│l│l│o│ ← Points to original buffer's memory
└─┴─┴─┴─┴─┘

After slice[0] = 74:
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│J│e│l│l│o│ │W│o│r│l│d│ ← Original also changed
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘

Creating True Copies:

const original = Buffer.from("Hello World");

// Method 1: Buffer.from() - creates a copy
const copy1 = Buffer.from(original.slice(0, 5));
copy1[0] = 74;
console.log(copy1.toString()); // "Jello"
console.log(original.toString()); // "Hello World" (unchanged)

// Method 2: Allocate new buffer and copy
const copy2 = Buffer.alloc(5);
original.copy(copy2, 0, 0, 5);
copy2[0] = 74;
console.log(copy2.toString()); // "Jello"
console.log(original.toString()); // "Hello World" (unchanged)

// Verify they're separate
console.log(copy1.buffer === original.buffer); // false
console.log(copy2.buffer === original.buffer); // false

buffer.subarray() - Explicit View Creation

subarray() is the modern, explicit way to create views (same as slice()):

const original = Buffer.from("Hello World");

// subarray() and slice() are identical for Buffers
const view1 = original.slice(0, 5);
const view2 = original.subarray(0, 5);

console.log(view1.toString()); // "Hello"
console.log(view2.toString()); // "Hello"

// Both are views
view1[0] = 74;
console.log(original.toString()); // "Jello World"

view2[1] = 65; // 'A'
console.log(original.toString()); // "JAllo World"

// Recommendation: Use subarray() for clarity
// It makes it obvious you're creating a view, not a copy

Practical Slicing Examples

// Example 1: Extract header from binary data
const fileData = Buffer.from([
0xff,
0xd8,
0xff,
0xe0, // JPEG header
0x00,
0x10, // Length
// ... rest of file
0x01,
0x02,
0x03,
0x04,
]);

const header = fileData.subarray(0, 4);
console.log(header); // <Buffer ff d8 ff e0>

// Check if it's a JPEG
if (header[0] === 0xff && header[1] === 0xd8) {
console.log("This is a JPEG file!");
}

// Example 2: Process chunks
const data = Buffer.from("AAAABBBBCCCCDDDD");
const chunkSize = 4;

for (let i = 0; i < data.length; i += chunkSize) {
const chunk = data.subarray(i, i + chunkSize);
console.log(`Chunk ${i / chunkSize}: ${chunk.toString()}`);
}
// Output:
// Chunk 0: AAAA
// Chunk 1: BBBB
// Chunk 2: CCCC
// Chunk 3: DDDD

// Example 3: Extract specific fields from binary protocol
const packet = Buffer.from([
0x01,
0x02, // Type (2 bytes)
0x00,
0x0a, // Length (2 bytes)
0x48,
0x65,
0x6c,
0x6c,
0x6f, // Data ("Hello")
0x00,
0x00,
0x00,
0x00,
0x00, // Padding
]);

const type = packet.subarray(0, 2);
const length = packet.subarray(2, 4);
const payload = packet.subarray(4, 4 + packet.readUInt16BE(2));

console.log("Type:", type.toString("hex")); // "0102"
console.log("Length:", length.readUInt16BE(0)); // 10
console.log("Data:", payload.toString()); // "Hello"

Buffer Copying

buffer.copy() - Copy Data Between Buffers

const source = Buffer.from("Hello World");
const target = Buffer.alloc(20);

// Copy entire source to target
source.copy(target);
console.log(target.toString()); // "Hello World" + zeros

// Full signature:
// source.copy(target, targetStart, sourceStart, sourceEnd)

// Example: Copy partial data
const source2 = Buffer.from("Hello World");
const target2 = Buffer.alloc(10);

// Copy "World" (bytes 6-11) to start of target2
source2.copy(target2, 0, 6, 11);
console.log(target2.toString("utf8", 0, 5)); // "World"

Visual Explanation:

source.copy(target, 2, 0, 5):

Source:
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│H│e│l│l│o│ │W│o│r│l│d│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
└─────────┘ Copy bytes 0-5

Target:
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│0│0│H│e│l│l│o│0│0│0│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
↑ Paste at index 2

Copy with Offset:

const source = Buffer.from("ABCD");
const target = Buffer.alloc(10).fill(45); // Fill with '-' (45)

// Copy to different positions
source.copy(target, 0); // Copy at start
source.copy(target, 5); // Copy at middle

console.log(target.toString());
// "ABCD-ABCD-" (two copies of source)

// Breakdown:
// Position 0-3: "ABCD"
// Position 4: "-"
// Position 5-8: "ABCD"
// Position 9: "-"

Handling Overflow:

const source = Buffer.from("Hello World"); // 11 bytes
const target = Buffer.alloc(5); // Only 5 bytes

// Copy will truncate to fit target
const bytesCopied = source.copy(target);

console.log(`Copied ${bytesCopied} bytes`); // 5
console.log(target.toString()); // "Hello" (truncated)

// Safe copying pattern:
function safeCopy(source: Buffer, target: Buffer, targetOffset = 0): number {
const spaceAvailable = target.length - targetOffset;
const bytesToCopy = Math.min(source.length, spaceAvailable);

if (bytesToCopy < source.length) {
console.log(
`⚠️ Truncating: ${source.length} bytes to ${bytesToCopy} bytes`
);
}

return source.copy(target, targetOffset, 0, bytesToCopy);
}

Buffer.concat() - Combine Multiple Buffers

const buf1 = Buffer.from("Hello ");
const buf2 = Buffer.from("World");
const buf3 = Buffer.from("!");

// Concatenate buffers
const combined = Buffer.concat([buf1, buf2, buf3]);

console.log(combined.toString()); // "Hello World!"
console.log(combined.length); // 12 bytes

// How it works internally:
// 1. Calculate total length: 6 + 5 + 1 = 12
// 2. Allocate new buffer of 12 bytes
// 3. Copy buf1 to position 0
// 4. Copy buf2 to position 6
// 5. Copy buf3 to position 11

Specify Total Length:

const buf1 = Buffer.from("Hello");
const buf2 = Buffer.from("World");

// Specify total length (useful if you know it)
const combined1 = Buffer.concat([buf1, buf2], 10);
console.log(combined1.toString()); // "HelloWorld"

// If specified length is larger, fills with zeros
const combined2 = Buffer.concat([buf1, buf2], 20);
console.log(combined2.length); // 20
console.log(combined2.toString()); // "HelloWorld" + zeros

// If specified length is smaller, truncates
const combined3 = Buffer.concat([buf1, buf2], 7);
console.log(combined3.toString()); // "HelloWo" (truncated)

Practical Example: Building Protocol Messages:

// Build a simple protocol message
function buildMessage(type: number, data: string): Buffer {
const header = Buffer.alloc(4);
header.writeUInt16BE(type, 0); // Message type (2 bytes)
header.writeUInt16BE(data.length, 2); // Data length (2 bytes)

const payload = Buffer.from(data);

return Buffer.concat([header, payload]);
}

const message = buildMessage(1, "Hello");
console.log(message);
// <Buffer 00 01 00 05 48 65 6c 6c 6f>
// [type][len ][H][e][l][l][o]

// Parse it back
function parseMessage(buffer: Buffer) {
const type = buffer.readUInt16BE(0);
const length = buffer.readUInt16BE(2);
const data = buffer.toString("utf8", 4, 4 + length);

return { type, length, data };
}

const parsed = parseMessage(message);
console.log(parsed); // { type: 1, length: 5, data: 'Hello' }

Buffer Comparison and Searching

buffer.equals() - Compare Entire Buffers

const buf1 = Buffer.from("Hello");
const buf2 = Buffer.from("Hello");
const buf3 = Buffer.from("World");

// Compare buffers
console.log(buf1.equals(buf2)); // true (same content)
console.log(buf1.equals(buf3)); // false (different content)

// Even if same reference
console.log(buf1.equals(buf1)); // true

// Note: This compares CONTENT, not reference
console.log(buf1 === buf2); // false (different objects)
console.log(buf1.equals(buf2)); // true (same content)

Case-Sensitive Comparison:

const buf1 = Buffer.from("Hello");
const buf2 = Buffer.from("hello");

console.log(buf1.equals(buf2)); // false (case matters!)

// For case-insensitive comparison:
const lower1 = Buffer.from(buf1.toString().toLowerCase());
const lower2 = Buffer.from(buf2.toString().toLowerCase());
console.log(lower1.equals(lower2)); // true

buffer.compare() - Lexicographic Comparison

const buf1 = Buffer.from("ABC");
const buf2 = Buffer.from("BCD");
const buf3 = Buffer.from("ABC");

// compare() returns:
// - Negative if buf1 < buf2
// - Zero if buf1 === buf2
// - Positive if buf1 > buf2

console.log(buf1.compare(buf2)); // -1 (buf1 < buf2)
console.log(buf2.compare(buf1)); // 1 (buf2 > buf1)
console.log(buf1.compare(buf3)); // 0 (buf1 === buf3)

// Compare portions
const buf4 = Buffer.from("Hello World");
const buf5 = Buffer.from("World");

// Compare "World" part of buf4 with buf5
const result = buf4.compare(buf5, 0, 5, 6, 11);
console.log(result); // 0 (they match)

// Signature:
// buf.compare(target, targetStart, targetEnd, sourceStart, sourceEnd)

Sorting Buffers:

const buffers = [
Buffer.from("zebra"),
Buffer.from("apple"),
Buffer.from("mango"),
Buffer.from("banana"),
];

// Sort using compare()
buffers.sort((a, b) => a.compare(b));

console.log(buffers.map((buf) => buf.toString()));
// ["apple", "banana", "mango", "zebra"]

buffer.indexOf() - Search for Value

const buffer = Buffer.from("Hello World Hello");

// Find first occurrence
console.log(buffer.indexOf("o")); // 4 (first 'o' in "Hello")

// Find with starting offset
console.log(buffer.indexOf("o", 5)); // 7 (next 'o' in "World")

// Find using buffer
const search = Buffer.from("World");
console.log(buffer.indexOf(search)); // 6

// Find using byte value
console.log(buffer.indexOf(111)); // 4 ('o' = 111)

// Not found returns -1
console.log(buffer.indexOf("xyz")); // -1

Find All Occurrences:

function findAll(buffer: Buffer, search: string | Buffer): number[] {
const positions: number[] = [];
let index = 0;

while (true) {
index = buffer.indexOf(search, index);
if (index === -1) break;

positions.push(index);
index++; // Move past this occurrence
}

return positions;
}

const buffer = Buffer.from("Hello World Hello Universe Hello");
const positions = findAll(buffer, "Hello");
console.log(positions); // [0, 12, 27]

// Verify:
positions.forEach((pos) => {
console.log(buffer.toString("utf8", pos, pos + 5)); // "Hello"
});

buffer.lastIndexOf() - Search Backwards

const buffer = Buffer.from("Hello World Hello");

// Find last occurrence
console.log(buffer.lastIndexOf("Hello")); // 12 (last "Hello")

// Search backwards from position
console.log(buffer.lastIndexOf("Hello", 10)); // 0 (search from pos 10 backwards)

// Not found
console.log(buffer.lastIndexOf("xyz")); // -1

buffer.includes() - Check if Contains Value

const buffer = Buffer.from("Hello World");

// Check if contains string
console.log(buffer.includes("World")); // true
console.log(buffer.includes("xyz")); // false

// Check if contains buffer
const search = Buffer.from("World");
console.log(buffer.includes(search)); // true

// Check if contains byte value
console.log(buffer.includes(111)); // true ('o' = 111)
console.log(buffer.includes(255)); // false

// With starting offset
console.log(buffer.includes("o", 5)); // true (second 'o' in "World")
console.log(buffer.includes("o", 8)); // false (no 'o' after position 8)

Buffer Filling

buffer.fill() - Fill Buffer with Value

// Fill entire buffer with a value
const buf1 = Buffer.alloc(10);
buf1.fill(65); // Fill with 'A' (ASCII 65)
console.log(buf1.toString()); // "AAAAAAAAAA"

// Fill with string
const buf2 = Buffer.alloc(10);
buf2.fill("abc"); // Pattern repeats
console.log(buf2.toString()); // "abcabcabca"

// Fill with buffer
const buf3 = Buffer.alloc(10);
const pattern = Buffer.from([1, 2, 3]);
buf3.fill(pattern);
console.log(buf3); // <Buffer 01 02 03 01 02 03 01 02 03 01>

// Fill portion of buffer
const buf4 = Buffer.alloc(10).fill(45); // All '-'
buf4.fill("X", 2, 8); // Fill bytes 2-7 with 'X'
console.log(buf4.toString()); // "--XXXXXX--"

Visual Representation:

buf.fill("ABC", 2, 8):

Before (all zeros):
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│0│0│0│0│0│0│0│0│0│0│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
0 1 2 3 4 5 6 7 8 9

After fill("ABC", 2, 8):
┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
│0│0│A│B│C│A│B│C│0│0│
└─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
└─────────┘ Filled region

Practical Fill Patterns:

// Clear buffer (zero out)
function clearBuffer(buffer: Buffer): void {
buffer.fill(0);
}

// Initialize with pattern
function initializeBuffer(size: number, pattern: string): Buffer {
const buffer = Buffer.alloc(size);
buffer.fill(pattern);
return buffer;
}

// Create delimiter-separated buffer
function createDelimitedBuffer(): Buffer {
const buffer = Buffer.alloc(20).fill(45); // Fill with '-'
buffer.write("DATA", 0);
buffer.write("DATA", 8);
buffer.write("DATA", 16);
return buffer;
}

console.log(createDelimitedBuffer().toString());
// "DATA----DATA----DATA"

Reading and Writing Multi-Byte Integers

Buffers provide methods to read and write integers of different sizes (8-bit, 16-bit, 32-bit) and byte orders (little-endian, big-endian).

Understanding Endianness

Endianness determines the order in which bytes are stored for multi-byte values.

Number: 0x12345678 (4 bytes)

Big-Endian (BE): Most significant byte first
Memory: [0x12][0x34][0x56][0x78]
↑ highest byte first
Used by: Network protocols, Java

Little-Endian (LE): Least significant byte first
Memory: [0x78][0x56][0x34][0x12]
↑ lowest byte first
Used by: x86/x64 CPUs, Windows

8-Bit Integer Operations (1 byte)

const buffer = Buffer.alloc(10);

// Write unsigned 8-bit integer (0-255)
buffer.writeUInt8(255, 0);
buffer.writeUInt8(128, 1);
buffer.writeUInt8(0, 2);

console.log(buffer.slice(0, 3)); // <Buffer ff 80 00>

// Read unsigned 8-bit integer
console.log(buffer.readUInt8(0)); // 255
console.log(buffer.readUInt8(1)); // 128
console.log(buffer.readUInt8(2)); // 0

// Write signed 8-bit integer (-128 to 127)
buffer.writeInt8(-1, 3);
buffer.writeInt8(-128, 4);
buffer.writeInt8(127, 5);

console.log(buffer.slice(3, 6)); // <Buffer ff 80 7f>

// Read signed 8-bit integer
console.log(buffer.readInt8(3)); // -1
console.log(buffer.readInt8(4)); // -128
console.log(buffer.readInt8(5)); // 127

Signed vs Unsigned:

const buffer = Buffer.from([0xff, 0x80, 0x7f]);

// As unsigned (0-255)
console.log(buffer.readUInt8(0)); // 255
console.log(buffer.readUInt8(1)); // 128
console.log(buffer.readUInt8(2)); // 127

// As signed (-128 to 127)
console.log(buffer.readInt8(0)); // -1 (0xFF = -1 in two's complement)
console.log(buffer.readInt8(1)); // -128 (0x80 = -128)
console.log(buffer.readInt8(2)); // 127 (0x7F = 127)

// Why different?
// 0xFF in binary: 11111111
// Unsigned: 255
// Signed: -1 (two's complement)

16-Bit Integer Operations (2 bytes)

const buffer = Buffer.alloc(10);

// Write 16-bit unsigned integer - Little Endian
buffer.writeUInt16LE(1000, 0);
console.log(buffer.slice(0, 2)); // <Buffer e8 03>
// 1000 = 0x03E8
// Little-endian: [0xE8, 0x03] (low byte first)

// Write 16-bit unsigned integer - Big Endian
buffer.writeUInt16BE(1000, 2);
console.log(buffer.slice(2, 4)); // <Buffer 03 e8>
// Big-endian: [0x03, 0xE8] (high byte first)

// Read back
console.log(buffer.readUInt16LE(0)); // 1000
console.log(buffer.readUInt16BE(2)); // 1000

// Signed 16-bit integers
buffer.writeInt16LE(-1000, 4);
buffer.writeInt16BE(-1000, 6);

console.log(buffer.readInt16LE(4)); // -1000
console.log(buffer.readInt16BE(6)); // -1000

Visual Comparison:

Value: 1000 (0x03E8 in hex)

Little-Endian (LE):
┌────┬────┐
│ E8 │ 03 │ Low byte first
└────┴────┘
↑ ↑
Low High

Big-Endian (BE):
┌────┬────┐
│ 03 │ E8 │ High byte first
└────┴────┘
↑ ↑
High Low

Reading with wrong endianness:
writeUInt16LE(1000) = [0xE8, 0x03]
readUInt16BE() = 0xE803 = 59395 (WRONG!)

32-Bit Integer Operations (4 bytes)

const buffer = Buffer.alloc(20);

// Write 32-bit unsigned integer
buffer.writeUInt32LE(1000000, 0);
buffer.writeUInt32BE(1000000, 4);

console.log(buffer.slice(0, 4)); // <Buffer 40 42 0f 00> (LE)
console.log(buffer.slice(4, 8)); // <Buffer 00 0f 42 40> (BE)

// Read back
console.log(buffer.readUInt32LE(0)); // 1000000
console.log(buffer.readUInt32BE(4)); // 1000000

// Signed 32-bit integers
buffer.writeInt32LE(-1000000, 8);
buffer.writeInt32BE(-1000000, 12);

console.log(buffer.readInt32LE(8)); // -1000000
console.log(buffer.readInt32BE(12)); // -1000000

// Maximum values
const maxInt32 = 2147483647; // 2^31 - 1
const maxUInt32 = 4294967295; // 2^32 - 1

buffer.writeInt32LE(maxInt32, 16);
console.log(buffer.readInt32LE(16)); // 2147483647

buffer.writeUInt32LE(maxUInt32, 16);
console.log(buffer.readUInt32LE(16)); // 4294967295

Floating-Point Operations

const buffer = Buffer.alloc(20);

// Write 32-bit float (single precision)
buffer.writeFloatLE(3.14159, 0);
buffer.writeFloatBE(3.14159, 4);

console.log(buffer.readFloatLE(0)); // 3.1415927410125732 (slight precision loss)
console.log(buffer.readFloatBE(4)); // 3.1415927410125732

// Write 64-bit double (double precision)
buffer.writeDoubleLE(3.14159265359, 8);
buffer.writeDoubleBE(3.14159265359, 16);

console.log(buffer.readDoubleLE(8)); // 3.14159265359 (exact)
console.log(buffer.readDoubleBE(16)); // 3.14159265359

// Why use doubles?
const preciseValue = 1234567890.123456789;

buffer.writeFloatLE(preciseValue, 0);
buffer.writeDoubleLE(preciseValue, 4);

console.log("Float:", buffer.readFloatLE(0)); // 1234567936 (precision lost!)
console.log("Double:", buffer.readDoubleLE(4)); // 1234567890.123457 (much better)

Practical Example: Binary File Format

// Create a simple binary file format
interface FileHeader {
magic: number; // 4 bytes: Magic number (0x4D59464D = "MYFM")
version: number; // 2 bytes: Version
flags: number; // 2 bytes: Flags
timestamp: number; // 4 bytes: Unix timestamp
dataLength: number; // 4 bytes: Data length
}

function writeHeader(header: FileHeader): Buffer {
const buffer = Buffer.alloc(16); // 4+2+2+4+4 = 16 bytes

buffer.writeUInt32BE(header.magic, 0); // Magic at 0
buffer.writeUInt16BE(header.version, 4); // Version at 4
buffer.writeUInt16BE(header.flags, 6); // Flags at 6
buffer.writeUInt32BE(header.timestamp, 8); // Timestamp at 8
buffer.writeUInt32BE(header.dataLength, 12); // Length at 12

return buffer;
}

function readHeader(buffer: Buffer): FileHeader {
return {
magic: buffer.readUInt32BE(0),
version: buffer.readUInt16BE(4),
flags: buffer.readUInt16BE(6),
timestamp: buffer.readUInt32BE(8),
dataLength: buffer.readUInt32BE(12),
};
}

// Usage
const header: FileHeader = {
magic: 0x4d59464d, // "MYFM"
version: 1,
flags: 0x0003,
timestamp: Math.floor(Date.now() / 1000),
dataLength: 1024,
};

const headerBuffer = writeHeader(header);
console.log(headerBuffer);
// <Buffer 4d 59 46 4d 00 01 00 03 ...>

const parsedHeader = readHeader(headerBuffer);
console.log(parsedHeader);
// { magic: 1297371981, version: 1, flags: 3, ... }

// Verify magic number
const magicString = String.fromCharCode(
(parsedHeader.magic >> 24) & 0xff,
(parsedHeader.magic >> 16) & 0xff,
(parsedHeader.magic >> 8) & 0xff,
parsedHeader.magic & 0xff
);
console.log("Magic:", magicString); // "MYFM"

Buffer Swapping Operations

Node.js provides methods to swap byte order for 16-bit, 32-bit, and 64-bit values:

// 16-bit swap
const buf16 = Buffer.from([0x01, 0x02, 0x03, 0x04]);
console.log(buf16); // <Buffer 01 02 03 04>

buf16.swap16(); // Swap every 2 bytes
console.log(buf16); // <Buffer 02 01 04 03>

// 32-bit swap
const buf32 = Buffer.from([0x01, 0x02, 0x03, 0x04]);
console.log(buf32); // <Buffer 01 02 03 04>

buf32.swap32(); // Swap every 4 bytes
console.log(buf32); // <Buffer 04 03 02 01>

// 64-bit swap
const buf64 = Buffer.from([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]);
console.log(buf64); // <Buffer 01 02 03 04 05 06 07 08>

buf64.swap64(); // Swap every 8 bytes
console.log(buf64); // <Buffer 08 07 06 05 04 03 02 01>

// Use case: Converting endianness
const value = 0x12345678;
const buffer = Buffer.alloc(4);
buffer.writeUInt32BE(value, 0); // Write as big-endian

console.log(buffer); // <Buffer 12 34 56 78>

buffer.swap32(); // Convert to little-endian
console.log(buffer); // <Buffer 78 56 34 12>
console.log(buffer.readUInt32LE(0)); // 305419896 (0x12345678)

Common Pitfalls and Solutions

Pitfall 1: Confusing slice() with Copy

// ❌ Problem: Thinking slice() creates a copy
const original = Buffer.from("Hello");
const slice = original.slice(0, 3);
slice[0] = 88; // Change 'H' to 'X'

console.log(original.toString()); // "Xello" (original changed!)

// ✅ Solution: Use Buffer.from() for true copy
const original2 = Buffer.from("Hello");
const copy = Buffer.from(original2.slice(0, 3));
copy[0] = 88;

console.log(original2.toString()); // "Hello" (unchanged)
console.log(copy.toString()); // "Xel"

Pitfall 2: Wrong Endianness

// ❌ Problem: Reading with wrong endianness
const buffer = Buffer.alloc(4);
buffer.writeUInt32LE(1000, 0); // Write little-endian

const wrong = buffer.readUInt32BE(0); // Read big-endian
console.log(wrong); // 3925868544 (completely wrong!)

const correct = buffer.readUInt32LE(0); // Read little-endian
console.log(correct); // 1000 (correct)

// ✅ Solution: Always match read/write endianness
// Convention: Use BE for network protocols, LE for file formats (document it!)

Pitfall 3: Buffer Overflow on Write

// ❌ Problem: Writing beyond buffer bounds
const buffer = Buffer.alloc(4);
buffer.writeUInt32LE(1000, 0); // OK
buffer.writeUInt32LE(2000, 2); // DANGER: Writes bytes 2-5, but buffer is only 4 bytes!

// This will write partial data and may corrupt adjacent memory
console.log(buffer); // <Buffer e8 03 d0 07>
// Only bytes 2-3 are written (partial 32-bit value)

// ✅ Solution: Always check bounds
function safeWriteUInt32(
buffer: Buffer,
value: number,
offset: number
): boolean {
if (offset + 4 > buffer.length) {
console.log("⚠️ Not enough space for UInt32");
return false;
}
buffer.writeUInt32LE(value, offset);
return true;
}

Pitfall 4: Forgetting toString() Encoding

// ❌ Problem: Implicit UTF-8 conversion loses data
const buffer = Buffer.from([0xff, 0xfe, 0xfd]);
console.log(buffer.toString()); // "���" (UTF-8 tries to decode as characters)

// ✅ Solution: Use appropriate encoding
console.log(buffer.toString("hex")); // "fffefd" (correct for binary data)
console.log(buffer.toString("base64")); // "//79" (correct for binary data)

// For raw binary display:
console.log(buffer); // <Buffer ff fe fd> (best for debugging)

Pitfall 5: Comparing Buffers with ===

// ❌ Problem: Using === compares references, not content
const buf1 = Buffer.from("Hello");
const buf2 = Buffer.from("Hello");

console.log(buf1 === buf2); // false (different objects)
console.log(buf1 == buf2); // false (still different objects)

// ✅ Solution: Use equals() for content comparison
console.log(buf1.equals(buf2)); // true (same content)

// Or compare() for ordering
console.log(buf1.compare(buf2)); // 0 (equal)

Pitfall 6: Modifying Shared Buffers

// ❌ Problem: Multiple references to same buffer
const original = Buffer.from("Hello");
const ref1 = original;
const ref2 = original.slice(0, 5); // Still a view!

ref1[0] = 88;
console.log(ref2.toString()); // "Xello" (all references affected)

// ✅ Solution: Be aware of references and use copies when needed
const original2 = Buffer.from("Hello");
const copy1 = Buffer.from(original2); // True copy
const copy2 = Buffer.from(original2); // Another copy

copy1[0] = 88;
console.log(original2.toString()); // "Hello" (unchanged)
console.log(copy2.toString()); // "Hello" (unchanged)

Summary: Key Takeaways

Basic Operations:

  • Direct byte access: Use buffer[index] to read/write individual bytes (0-255)
  • String conversion: toString(encoding) converts to string, write() writes string to buffer
  • Length properties: buffer.length is byte count (not character count)

Slicing and Copying:

  • slice() creates VIEWS - modifications affect the original buffer
  • Use Buffer.from() to create true copies of slices
  • copy() explicitly copies data between buffers
  • concat() combines multiple buffers into one new buffer

Comparison and Search:

  • equals() - Compare buffer contents (returns boolean)
  • compare() - Lexicographic comparison (returns -1, 0, or 1)
  • indexOf() - Find first occurrence
  • lastIndexOf() - Find last occurrence
  • includes() - Check if contains value

Filling:

  • fill(value) - Fill entire buffer or portion with value
  • Patterns repeat if value is shorter than fill region

Multi-Byte Integers:

  • 8-bit: readUInt8, writeUInt8, readInt8, writeInt8
  • 16-bit: readUInt16LE/BE, writeUInt16LE/BE, readInt16LE/BE, writeInt16LE/BE
  • 32-bit: readUInt32LE/BE, writeUInt32LE/BE, readInt32LE/BE, writeInt32LE/BE
  • Floats: readFloatLE/BE, writeFloatLE/BE, readDoubleLE/BE, writeDoubleLE/BE
  • Endianness matters: LE (little-endian) for x86, BE (big-endian) for network protocols

Common Mistakes:

  • Treating slice() as copy (it's a view!)
  • Using wrong endianness for read/write
  • Writing beyond buffer bounds
  • Comparing buffers with === instead of equals()
  • Forgetting that string length ≠ buffer length for UTF-8

What's Next?

Now that you've mastered Buffer operations, continue with:

  • Buffer Encoding and Conversion - Deep dive into character encodings (UTF-8, UTF-16, Base64, Hex), converting between formats, and handling international text