A checksum job should not need to load an entire archive into memory. Node's readable streams can supply binary chunks incrementally, and a hash can consume each chunk as it arrives. This example adds a total-byte cap and returns a digest only after the source ends successfully. A stream error or excessive input rejects the operation instead of producing a misleading partial checksum.
Prerequisites and input contract
Use a maintained Node.js release with ES modules and built-in crypto and streams support. The referenced APIs are documented in Node 22; the actual installed runtime used for execution was Node 26.13.0. No npm packages are required. Save the program with the .mjs extension so its imports and top-level await work directly.
The function accepts an asynchronous iterable of Uint8Array chunks, including Buffers, and a nonnegative safe-integer byte limit. It deliberately refuses strings: hashing original bytes should not depend on an implicit text encoding. Each chunk contributes its byteLength to the total before it is fed into SHA-256. Input exactly at the cap is accepted; the first chunk crossing it triggers rejection.
Complete example
Save as digest.mjs.
import { createHash } from 'node:crypto';
import { Readable } from 'node:stream';
import assert from 'node:assert/strict';
async function sha256(source, limit) {
if (!Number.isSafeInteger(limit) || limit < 0) throw new RangeError('invalid limit');
const hash = createHash('sha256');
let bytes = 0;
for await (const chunk of source) {
if (!(chunk instanceof Uint8Array)) throw new TypeError('binary chunks required');
bytes += chunk.byteLength;
if (bytes > limit) throw new RangeError('byte limit exceeded');
hash.update(chunk);
}
return { bytes, hex: hash.digest('hex') };
}
const result = await sha256(Readable.from([Buffer.from('a'), Buffer.from('bc')]), 3);
assert.equal(result.hex, 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad');
assert.equal(result.bytes, 3);
const whole = await sha256(Readable.from([Buffer.from('abc')]), 3);
assert.deepEqual(whole, result);
const empty = await sha256(Readable.from([]), 0);
assert.equal(empty.hex, 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
await assert.rejects(sha256(Readable.from([Buffer.from('abcd')]), 3), RangeError);
await assert.rejects(sha256(Readable.from(['abc']), 3), TypeError);
async function* broken() { yield Buffer.from('a'); throw new Error('source failed'); }
await assert.rejects(sha256(Readable.from(broken()), 3), /source failed/);
console.log(`bytes=${result.bytes} sha256=${result.hex}`);
console.log('chunking, empty, oversize, type and source-error checks passed');
Run and expected output
node digest.mjsbytes=3 sha256=ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
chunking, empty, oversize, type and source-error checks passed
What the checks establish
The fixture hashes a followed by bc and compares the result with the known SHA-256 digest of abc. It also hashes abc as a single chunk to verify that the digest is independent of that chunk boundary. An empty stream is checked against its known empty-input digest. Further checks reject excess bytes, text chunks and a source that throws after yielding one chunk.
These fixtures use real Node streams and the actual crypto implementation. No file or network input is mocked as though it had been read: the source is an explicitly constructed in-memory stream. A production caller can supply a binary file readable while retaining the same async iteration boundary, but that file path and its permissions need their own checks.
Edge cases and troubleshooting
The cap limits bytes accepted by this function, not the size of an individual chunk already allocated upstream. Configure source buffering and timeouts separately if you are receiving large or slow uploads. This function has no timeout or cancellation parameter. It also does not compare an expected digest; if you add verification, validate the expected value and define where trusted checksum metadata comes from.
Do not set a text encoding on the source unless you intentionally redesign the contract to hash encoded text. That produces strings and this implementation will reject them. A digest covers exactly the bytes provided; newline conversion or Unicode normalization changes those bytes. SHA-256 alone does not authenticate who supplied a file. The test validates deterministic hashing and error propagation, with no throughput benchmark, real upload, file race, or remote sender involved.
Primary references
Continue with node.js tutorials.