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.