hash.digest()

Finalizes a Hash and returns the digest of everything passed to update(), as a Buffer or string.

Signature
hash.digest([encoding])
  • Added in Node.js v0.1.92

Parameters

ParameterTypeDescription
$encodingoptionalstring= a Buffer is returned when omittedThe encoding of the return value, such as 'hex', 'base64', 'base64url' or 'latin1'. Omit it to receive a Buffer of raw bytes.

Return value

Buffer | stringA string in the requested encoding when encoding is given, otherwise a Buffer containing the raw digest bytes.

Errors

Throws ERR_CRYPTO_HASH_FINALIZED if digest() is called more than once on the same Hash object.

hash.digest() closes a Hash and hands back the fixed-size fingerprint of everything fed in through update(). The output size depends only on the algorithm, never on the input: SHA-256 always yields 32 bytes whether you hashed one character or one terabyte.

Buffer or string

With no argument you get a Buffer. Pass 'hex' for the familiar 64-character SHA-256 string, 'base64' when it goes in a JSON body or header, or 'base64url' when it goes in a URL or a JWT and you cannot afford +, / and =. Prefer the raw Buffer when the digest is about to be compared with crypto.timingSafeEqual(), which needs buffers anyway.

What trips people up

The Hash is single-use. After digest() returns, both a second digest() and any further update() throw ERR_CRYPTO_HASH_FINALIZED. This bites hardest when a Hash is stashed in a module-level variable or a class field and reused across requests: the first request succeeds and every one after it throws. Build a new Hash per value.

The other trap is comparing digests with === or == when the result decides whether a request is authorised. String comparison short-circuits at the first differing character, which leaks how much of a guess was correct.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Producing a hex checksum to print or store in a manifest file
  • Getting a raw Buffer digest to hand to crypto.timingSafeEqual()
  • Encoding a digest as base64url for a URL-safe cache key or ETag
  • Comparing a downloaded file's digest against a published checksum

Frequently asked questions

Why does hash.digest() throw "Digest already called"?

Because a Hash object is finalised by its first digest() call and cannot be used again — the error code is ERR_CRYPTO_HASH_FINALIZED. Create a new object with crypto.createHash() for every value instead of caching one in a variable or class field.

Should hash.digest() return hex or a Buffer?

Use 'hex' when a human will read or store the value, 'base64url' when it goes in a URL or header, and no argument at all when the digest is about to be passed to crypto.timingSafeEqual(), which requires buffers. All three represent the same bytes.

Why is my SHA-256 digest always 64 characters?

SHA-256 produces 32 bytes regardless of input size, and hex encoding uses two characters per byte, so the hex form is always 64 characters. Base64 gives 44 characters and base64url 43 for the same digest.

More examples

The reused-Hash bug

The broken version works exactly once, then throws ERR_CRYPTO_HASH_FINALIZED forever. In a server this looks like an endpoint that succeeds on the first request after deploy and 500s on every request afterwards — a classic reused-Hash bug.

Example · javascript
const crypto = require('node:crypto');

// BROKEN: one Hash shared by every call.
const shared = crypto.createHash('sha256');
function brokenFingerprint(value) {
  return shared.update(value).digest('hex');
}

// CORRECT: a fresh Hash per value.
function fingerprint(value) {
  return crypto.createHash('sha256').update(value).digest('hex');
}

console.log('first broken call :', brokenFingerprint('a').slice(0, 16), '...');
try {
  brokenFingerprint('b');
} catch (err) {
  console.log('second broken call:', err.code);
}

console.log('correct #1:', fingerprint('a').slice(0, 16), '...');
console.log('correct #2:', fingerprint('b').slice(0, 16), '...');

Comparing digests safely

Taking the Buffer form straight from digest() feeds timingSafeEqual() directly with no re-encoding. Two digests of the same algorithm always share a byte length, so the length-mismatch error that timingSafeEqual throws on raw secrets cannot occur.

Example · javascript
const crypto = require('node:crypto');

const expected = crypto.createHash('sha256').update('the real body').digest();
const supplied = crypto.createHash('sha256').update('the real body').digest();
const wrong = crypto.createHash('sha256').update('a forged body').digest();

// Digests are the same fixed length, so timingSafeEqual never throws here.
console.log('match :', crypto.timingSafeEqual(expected, supplied));
console.log('forged:', crypto.timingSafeEqual(expected, wrong));

// Avoid this when the result gates access — === stops at the first
// differing character and the timing difference is measurable.
console.log('string compare also works, but leaks timing:',
  expected.toString('hex') === supplied.toString('hex'));

Verifying a download against a published checksum

Streaming into the Hash and calling digest('hex') once at the end reproduces exactly what sha256sum prints, so the value can be compared directly with a checksum published on a release page. A plain === is fine here because a corrupt download is not an attacker guessing a secret.

Example · javascript
import crypto from 'node:crypto';
import fs from 'node:fs';
import { pipeline } from 'node:stream/promises';

fs.writeFileSync('release.tar', 'hello world');

// The value a project would publish alongside its release.
const published = 'b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9';

const hash = crypto.createHash('sha256');
await pipeline(fs.createReadStream('release.tar'), hash);
const actual = hash.digest('hex');

console.log('expected:', published);
console.log('actual  :', actual);
console.log('verdict :', actual === published ? 'OK' : 'CORRUPT');

fs.unlinkSync('release.tar');

Discussion

  • Be the first to comment on this lesson.