Skip to content
JavaScript Basics: Build and Read Immutable Blobs

JavaScript Basics: Build and Read Immutable Blobs

October 24, 2026

A Blob packages bytes without exposing a writable view of its contents. It is useful when an API expects a single binary object assembled from strings or typed arrays. Start with small in-memory payloads before connecting them to files or HTTP requests.

These three independent examples were executed on Linux with Node.js v24.19.0. Use Node.js 24 for this tutorial; only that installed version was tested. They use built-in modules and assertions, with no package installation. They make no network requests and leave no data files behind. The output below is the observed output.

Prepare a scratch directory

You need Bash, Node.js on your PATH, a text editor, and the mktemp, rm, and rmdir utilities. Keep the same terminal open and save each named file as UTF-8 in the new directory. The .mjs extension selects an ES module.

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested runtime prints:

v24.19.0

1. Take a snapshot of mutable bytes

Save this as snapshot-blob-bytes.mjs:

import assert from 'node:assert/strict';
import { Blob } from 'node:buffer';

const source = new Uint8Array([79, 75]);
const payload = new Blob([source, '\n'], { type: 'text/plain' });
source.fill(88);

const text = await payload.text();
assert.equal(text, 'OK\n');
assert.equal(payload.size, 3);
assert.equal(payload.type, 'text/plain');
assert.deepEqual([...source], [88, 88]);
console.log(`text: ${text.trimEnd()}`);
console.log(`bytes: ${payload.size}; type: ${payload.type}`);
console.log(`source after edit: ${source.join(',')}`);

Run it:

node snapshot-blob-bytes.mjs

Output:

text: OK
bytes: 3; type: text/plain
source after edit: 88,88

The source array changes to two X bytes, but the Blob still contains OK and a newline. The Blob constructor copies typed-array inputs and encodes string parts as UTF-8. The type is a label, not validation that the payload really matches that media type.

2. Slice by byte offsets, not character positions

Save this as slice-blob-byte-ranges.mjs:

import assert from 'node:assert/strict';
import { Blob } from 'node:buffer';

const payload = new Blob(['AéB'], { type: 'text/plain' });
const middle = payload.slice(1, 3, 'text/plain');
const incomplete = payload.slice(1, 2);

assert.equal(payload.size, 4);
assert.equal(await middle.text(), 'é');
assert.equal(middle.size, 2);
assert.equal(await incomplete.text(), '\uFFFD');
assert.equal(incomplete.type, '');
assert.equal(await payload.text(), 'AéB');
console.log(`whole bytes: ${payload.size}`);
console.log(`middle text: ${await middle.text()}`);
console.log('split UTF-8 sequence: replacement character');

Run it:

node slice-blob-byte-ranges.mjs

Output:

whole bytes: 4
middle text: é
split UTF-8 sequence: replacement character

The character é occupies two UTF-8 bytes here. The end offset is exclusive: bytes 1 and 2 form the complete character. Keeping only byte 1 produces invalid UTF-8, which text() replaces during decoding. slice() creates another Blob without altering the original. Supply the new type explicitly when you need it.

3. Read a fresh copy or consume a stream

Save this as read-blob-copies-and-streams.mjs:

import assert from 'node:assert/strict';
import { Blob } from 'node:buffer';

const payload = new Blob([new Uint8Array([10, 20]), new Uint8Array([30])]);
const copy = new Uint8Array(await payload.arrayBuffer());
copy[0] = 99;
assert.deepEqual([...await payload.bytes()], [10, 20, 30]);

const collected = [];
for await (const chunk of payload.stream()) {
  for (const byte of chunk) collected.push(byte);
}
assert.deepEqual(collected, [10, 20, 30]);
console.log(`edited copy: ${copy.join(',')}`);
console.log(`streamed bytes: ${collected.join(',')}`);
console.log(`still readable: ${(await payload.bytes()).join(',')}`);

Run it:

node read-blob-copies-and-streams.mjs

Output:

edited copy: 99,20,30
streamed bytes: 10,20,30
still readable: 10,20,30

Changing the arrayBuffer() copy leaves the original intact. The stream() method exposes a Web ReadableStream; this Node.js example consumes it with asynchronous iteration. Do not assume a particular chunk size. The byte-by-byte loop deliberately avoids that assumption, and the final read checks that the Blob remains usable.

Practical limits

These payloads are intentionally tiny. arrayBuffer(), bytes(), and text() materialize the complete contents; collecting a stream into an array does too. Process chunks incrementally for larger inputs. A Blob is not a file path and these examples never save one to disk. For strict text validation, use a fatal TextDecoder rather than accepting replacement characters from text().

Clean up

In the same terminal, remove only the three named source files, return to your starting directory, and remove the now-empty scratch directory:

rm -- snapshot-blob-bytes.mjs slice-blob-byte-ranges.mjs read-blob-copies-and-streams.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on