Skip to content
snip tools

Hash a string in JavaScript with SHA-256 (Web Crypto)

A snip.tools guide · runs alongside the Hash generator

Prefer to just run it? Use the free Hash generator — no install, in your browser.

Open the Hash generator

A cryptographic hash turns any input into a fixed-size fingerprint: the same input always yields the same output, but you can't work backwards from the hash to the input, and changing a single byte scrambles the whole result. JavaScript can do this natively through the Web Crypto API — no library needed. Here's the SHA-256 one-liner, how to format the result, and the one rule that matters most: never use a plain hash for passwords. (To hash a value right now without writing any code, use our in-browser hash generator.)

SHA-256 with crypto.subtle.digest

The hashing entry point is crypto.subtle.digest. It takes an algorithm name and a buffer of bytes, and returns a promise of an ArrayBuffer — the raw digest. Two small conversions bookend it: text to UTF-8 bytes going in, and bytes to a hex string coming out.

// Hash a string with SHA-256 using the built-in Web Crypto API.
async function sha256(message) {
  const data = new TextEncoder().encode(message);      // string → UTF-8 bytes
  const hash = await crypto.subtle.digest('SHA-256', data); // → ArrayBuffer
  return [...new Uint8Array(hash)]                      // bytes → hex string
    .map((b) => b.toString(16).padStart(2, '0'))
    .join('');
}

await sha256('hello');
// "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"

It's asynchronous because the API is designed to allow off-thread and hardware-accelerated implementations, so you'll always await it. And like all of Web Crypto, it's only available in a secure context: https:// pages and localhost, but not plain http://.

The hex conversion, line by line

The digest is raw bytes; humans want hex. new Uint8Array(hash) views those bytes, each b.toString(16) renders one byte as hex, and padStart(2, '0') keeps the leading zero on values below 16 (so byte 5 is "05", not "5") — drop that and your hash silently comes out the wrong length. Joining gives the familiar 64-character SHA-256 string.

Other digest sizes

The same function does the rest of the SHA-2 family — just change the name:

await crypto.subtle.digest('SHA-256', data); // 64 hex chars
await crypto.subtle.digest('SHA-384', data); // 96 hex chars
await crypto.subtle.digest('SHA-512', data); // 128 hex chars
// 'SHA-1' is also supported but is broken — see below.

Use SHA-256 unless you have a specific reason for a longer digest. Note that MD5 is not available in Web Crypto at all, by design — it's considered unsafe (our hash generator implements MD5 separately for the legacy-checksum use case, but you shouldn't reach for it in new code).

Verifying a value against a known hash

A common task is checking whether some input matches a stored fingerprint — file integrity, a checksum, a cache key. Hash the input and compare hex strings:

// Verify a value against a known hash by comparing the hex strings.
async function matches(input, expectedHex) {
  return (await sha256(input)) === expectedHex.toLowerCase();
}

Why you must never hash passwords this way

This is the mistake that turns up in breach post-mortems. SHA-256 is built to be fast — modern hardware computes billions per second — which is exactly what an attacker with a leaked database wants. A plain (or even salted) SHA-256 of a password is brute-forceable. Passwords need a deliberately slow, salted, memory-hard function:

// DON'T do this for passwords — plain SHA-256 is far too fast.
const stored = await sha256(userPassword); // ❌ brute-forceable

// DO use a slow, salted password hash on the server (Argon2id / scrypt /
// bcrypt). Web Crypto's PBKDF2 is the browser-available option:
async function pbkdf2(password, saltBytes, iterations = 600_000) {
  const key = await crypto.subtle.importKey(
    'raw', new TextEncoder().encode(password), 'PBKDF2', false, ['deriveBits'],
  );
  const bits = await crypto.subtle.deriveBits(
    { name: 'PBKDF2', salt: saltBytes, iterations, hash: 'SHA-256' }, key, 256,
  );
  return new Uint8Array(bits);
}

The right answer for passwords is Argon2id (or scrypt/bcrypt) on the server. In the browser, Web Crypto offers PBKDF2 with a high iteration count, shown above. The general rule: SHA-256 is for integrity and fingerprints, not for protecting secrets.

Hashing is one-way — and not encryption

Finally, keep the categories straight. Hashing is irreversible by design: there's no "unhash". That's different from Base64 (reversible encoding, hides nothing) and from encryption (reversible with a key). If you need to read the data back, you want encryption, not a hash. To generate hashes interactively across MD5, SHA-1 and the SHA-2 family, the hash generator runs entirely in your browser — your input is never uploaded. Writing your own randomness or IDs nearby? See generating a UUID in JavaScript, which draws from the same Web Crypto module.

Frequently asked questions

How do I hash a string with SHA-256 in JavaScript?
Encode the string to bytes with new TextEncoder().encode(str), pass the bytes to await crypto.subtle.digest('SHA-256', bytes), then map the resulting ArrayBuffer to a hex string. It's built into the browser and Node — no library needed.
Why is crypto.subtle.digest asynchronous?
The Web Crypto API is designed to allow off-thread and hardware-accelerated implementations, so digest() returns a promise — you always await it. There is no synchronous equivalent in the browser.
Can I compute an MD5 hash with Web Crypto?
No. Web Crypto deliberately omits MD5 (and offers SHA-1 only for legacy interop) because both are considered unsafe. Use SHA-256 for new code; reach for MD5 only for non-security checksums, and via a separate implementation.
Is SHA-256 safe for hashing passwords?
No. SHA-256 is built to be fast, which is exactly what an attacker with a leaked database wants — a plain or even salted SHA-256 of a password is brute-forceable. Use a slow, salted function: Argon2id, scrypt or bcrypt on the server, or PBKDF2 with a high iteration count in the browser.
Why does my SHA-256 hash come out the wrong length?
Almost always a missing padStart(2, "0") in the hex conversion. Bytes below 16 render as a single hex digit, so byte 5 becomes "5" instead of "05" and the string ends up shorter than 64 characters. Pad each byte to two digits.
Does hashing work on http pages?
No. Like all of Web Crypto, crypto.subtle.digest is only available in a secure context: https:// pages and localhost, but not a plain http:// origin.

Try it now: the Hash generator does everything in this guide in your browser — nothing is uploaded. Browse more guides or the full tool list.