Generate a random string in JavaScript

Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('')

Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('') returns 32 hex characters from 16 bytes of the platform’s cryptographic random source: 128 bits, enough for a session ID, an API key or a reset token. It runs in every browser and in Node 19 and later, where crypto is a global.

Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, '0')).join('');
// '6b9e49fc8970925abba4339a13709fe1'

Keep the padStart: without it, the byte 0a becomes the single digit a and the string comes out short. When a UUID will do, crypto.randomUUID() gives 36 characters with 122 random bits. Browsers offer it only in secure contexts (HTTPS or localhost); getRandomValues works on plain HTTP too.

crypto.randomUUID();
// 'b7a63655-e199-4160-9159-a1c1d1a167c7'

Letters and digits, without modulo bias

The usual way to pick from an alphabet, alphabet[bytes[i] % alphabet.length], is biased. A byte has 256 values, and 256 = 4 × 62 + 8, so of 62 letters and digits the first eight, A to H, get five byte values each and the other 54 get four. A comes up 25% more often than Z. A million bytes show it:

const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const limit = 256 - (256 % alphabet.length); // 248
const modulo = {}, rejection = {};
for (let i = 0; i < 20; i++) {
  for (const byte of crypto.getRandomValues(new Uint8Array(50_000))) {
    const c = alphabet[byte % alphabet.length];
    modulo[c] = (modulo[c] ?? 0) + 1;
    if (byte < limit) rejection[c] = (rejection[c] ?? 0) + 1;
  }
}
console.log('modulo:   ', modulo.A, modulo.H, modulo.I, modulo.Z);
console.log('rejection:', rejection.A, rejection.H, rejection.I, rejection.Z);
modulo:    19685 19339 15523 15722
rejection: 15719 15416 15523 15722

The fix in the second line is rejection sampling: drop every byte of 248 or more, the largest multiple of 62 a byte can hold, and read another.

function randomString(length, alphabet) {
  const limit = 256 - (256 % alphabet.length);
  let out = '';
  while (out.length < length) {
    for (const byte of crypto.getRandomValues(new Uint8Array(length))) {
      if (byte < limit && out.length < length) out += alphabet[byte % alphabet.length];
    }
  }
  return out;
}

randomString(22, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789');
// 'omcP2yS0sFPLUUNk0jyXaA'

It handles alphabets of up to 256 characters. The tool below draws the same way: fed the 44 bytes that call took from getRandomValues, its core returns the same omcP2yS0sFPLUUNk0jyXaA, skipping the same byte, fa (250).

Why not Math.random

Math.random().toString(36).slice(2) is the most copied answer, and it’s wrong for tokens. The ECMAScript spec asks only for an approximately uniform number from an implementation-defined algorithm. V8, the engine in Chrome and Node, uses xorshift128+, whose state can be worked out from a handful of outputs, after which every value is known. Its length varies too: over 100,000 calls, 8 to 14 characters.

How long

Each character from an alphabet of N carries log2(N) bits, so 128 bits takes 32 hex characters, 22 base64url characters or 22 letters and digits. The tool shows the working: 32 characters × log2(16) = 128 bits, and for the string above, 22 characters × log2(62) = 131 bits. OWASP’s Session Management Cheat Sheet sets 64 bits of entropy as the floor for a session ID; 128 is the usual target.

Node

In Node, randomBytes does it in one call:

const { randomBytes } = require('node:crypto');
randomBytes(16).toString('hex');       // '6e6bedfaee79274d6ffd0a93113fe32a'
randomBytes(16).toString('base64url'); // 'Eylq3NchrFtCmpE9oSkLVQ'

One getRandomValues call fills at most 65,536 bytes; past that it throws a QuotaExceededError.

Try it in the Random String Generator

Drawing bytes from your browser’s random source…

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