Understanding random generation
Math.random isn't always random enough.
Three flavours of "random" in the browser, the difference between PRNG and CSPRNG, and when each is the right tool.
Math.random — fast, predictable, unsafe.
Math.random() returns a 64-bit float in [0, 1). Implemented as a pseudo-random number generator (PRNG); each browser uses a different algorithm (Chrome's xorshift128+, Firefox's xorshift+). Cryptographically unsafe — the internal state can be inferred from a small number of consecutive outputs. Fine for shuffling a UI list, picking a random colour, generating fake test data. Wrong for tokens, IDs, passwords, anything an attacker might benefit from predicting.
crypto.getRandomValues — cryptographically secure.
The Web Crypto API ships a CSPRNG (cryptographically secure pseudo-random number generator) at crypto.getRandomValues(typedArray). Fill a Uint8Array with random bytes; the bytes are unguessable.crypto.randomUUID() wraps it specifically for UUIDv4. For session tokens, password reset tokens, CSRF nonces, any security-sensitive random value — use Web Crypto, not Math.random.
The modulo bias trap.
The naive way to get a random integer in [0, N): take a uniformly-random byte, modulo N. If N doesn't divide 256 evenly, some outputs are slightly more likely than others. For N=100 the bias is small (256 % 100 = 56, so values 0-55 are slightly more common). For password generation against a large alphabet, the bias adds up to a noticeable entropy reduction over many characters. The fix: rejection sampling — pick a random byte, discard if it falls in the "tail" that would bias, retry.
A worked random integer.
Secure random integer in [0, 100) with no modulo bias: function secureRandomInt(max) {
const range = 256 - (256 % max);
let buf = new Uint8Array(1);
do { crypto.getRandomValues(buf); } while (buf[0] >= range);
return buf[0] % max;
} Generates a byte, rejects if it would cause bias, returns within range. For larger ranges, use more bytes (2 for < 65536, 4 for < 4 billion). The rejection rate is at most 50 %.
Rejection sampling for unbiased integers
discard the modulo-tail
Bytes in the rejected range get re-rolled.
range = 256 − (256 % max) ; reject if >= range
= Uniform in [0, max)
Seeded PRNGs for reproducibility.
The opposite use case: deterministic random. Same seed → same sequence. Useful for tests, procedurally-generated content, replays, animations that should look the same per visitor session. Math.random can't be seeded; use a library like seedrandom or random-js. The output is fully predictable from the seed, which is the entire point.
True random — only via hardware.
Even CSPRNGs are pseudo-random — given the internal state, the next output is determined. "True" random requires a hardware source: thermal noise, radioactive decay, atmospheric noise. random.org uses atmospheric-noise capture; modern CPUs have an instruction (RDRAND on x86) that wraps a hardware noise source. For 99.9 % of applications, CSPRNG is indistinguishable from true random; for one-time-pad-style cryptography or gambling regulators, hardware sources matter.