SHA-256 hash in Node.js

crypto.createHash('sha256').update(text).digest('hex')

crypto.createHash('sha256').update(text).digest('hex') returns the SHA-256 of a string as 64 hex characters. update reads a string as UTF-8 unless you pass another encoding.

import crypto from 'node:crypto';

crypto.createHash('sha256').update('café').digest('hex');
// '850f7dc43910ff890f8879c0ed26fe697c93a067ad93a7d50f466a7028a9bf4e'

The tool below shows the same digest in its SHA-256 row: it hashes the 5 UTF-8 bytes of café. Node 22 also has a one-shot crypto.hash('sha256', 'café'), which returns the same hex.

The bytes decide the hash

The encoding argument changes which bytes are hashed, and so the whole digest:

crypto.createHash('sha256').update('café', 'latin1').digest('hex');
// 'dafd66c0b98965e688be1fc12942c09f0350e6be0685017c3f234e97d0adc92e'

That’s the hash of the 4 bytes 63 61 66 e9; choose Hex bytes in the tool and paste 636166e9 to get it there too. When your digest doesn’t match another system’s, compare the bytes first: an encoding, a trailing newline or CRLF line endings.

digest() finishes the hash. A second call, or an update() after it, throws Error [ERR_CRYPTO_HASH_FINALIZED]: Digest already called, so create a new hash per value. digest('base64') gives Base64 instead of hex, and digest() with no argument gives a 32-byte Buffer.

Files

Stream the file through the hash, so it never sits in memory whole:

import { createReadStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

async function sha256File(path) {
  const hash = crypto.createHash('sha256');
  await pipeline(createReadStream(path), hash);
  return hash.digest('hex');
}
// a file holding "hello": '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'

In the browser

Browsers have no createHash. Web Crypto’s crypto.subtle.digest takes bytes and returns a promise of an ArrayBuffer, which you turn into hex yourself:

async function sha256(text) {
  const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
  return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, '0')).join('');
}

await sha256('café');
// '850f7dc43910ff890f8879c0ed26fe697c93a067ad93a7d50f466a7028a9bf4e'

Keep the padStart. Without it, every byte below 0x10 loses its leading zero, and café comes out as 61 characters: 85f7dc43910ff89f8879….

crypto.subtle exists only in secure contexts. On a page served over plain http://, other than localhost, it’s undefined, and the call fails with TypeError: Cannot read properties of undefined (reading 'digest'). Passing a string instead of bytes throws a TypeError too. The same code runs in Node 22, where crypto is a global.

For a keyed hash, use crypto.createHmac('sha256', key), not SHA-256 of the key and message joined together.

Try it in the Hash Generator

Nothing leaves your browser: every digest is worked out here, in the page.

Case and spaces are ignored, so a hash from a checksum file goes straight in.

Hashing the 5 bytes of UTF-8 text.

AlgorithmDigestCopy
MD5 · 128 bits · broken07117fe4a1ebd544965dc19573183da2
SHA-1 · 160 bits · brokenf424452a9673918c6f09b0cdd35b20be8e6ae7d7
SHA-256 · 256 bits850f7dc43910ff890f8879c0ed26fe697c93a067ad93a7d50f466a7028a9bf4e
SHA-384 · 384 bits286ef187ce8fa0c9b3096a918ec6c4c96cb825267802c44f4e0d765728da7b1baf026fc292e273124d886911737e3689
SHA-512 · 512 bits0c9dac7fe613719170790f08a5f7b9f5ef876c7b57ff429074bf417969c2c54107d924daf5e706568afca4712d91da1cfdf77588d76403a845177e23e3aeb8ce

MD5 and SHA-1 are broken: collisions can be built on purpose, so a digest that matches proves nothing about who wrote a file. Use SHA-256 to check a file you downloaded. For passwords, use neither: bcrypt or Argon2 is slow on purpose and salts every hash, so a stolen list can’t be cracked in bulk.

Runs in your browser. Nothing you enter is sent anywhere.