Randomness in JavaScript — Why Math.random() Isn't Random Enough
Building a fair lottery picker requires more than Math.random(). Here's the difference between pseudo-random and cryptographically secure random, and when it matters.
Math.random() — Fast but Predictable
Math.random(); // 0.7234... This uses a PRNG (pseudo-random number generator) — typically xorshift128+ in V8. It's fast but deterministic: given the same seed, the sequence is reproducible.
For games and UI animations, this is fine. For lottery picks and security tokens, it isn't.
crypto.getRandomValues() — Truly Random
function secureRandom() { const array = new Uint32Array(1); crypto.getRandomValues(array); return array[0] / (0xFFFFFFFF + 1); // 0 to 1 } This uses the OS entropy pool (hardware events, timing jitter) — genuinely unpredictable.
Fair Shuffle (Fisher-Yates)
The naive shuffle (sort(() => Math.random() - 0.5)) is biased. Use Fisher-Yates:
function fairShuffle(array) { const arr = [...array]; for (let i = arr.length - 1; i > 0; i--) { const j = Math.floor(secureRandom() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } return arr; } Drawing Without Replacement
function drawWithoutReplacement(items, count) { if (count > items.length) throw new Error('Cannot draw more than available'); return fairShuffle(items).slice(0, count); } Drawing With Replacement
function drawWithReplacement(items, count) { return Array.from({ length: count }, () => { const index = Math.floor(secureRandom() * items.length); return items[index]; }); } Random Number in Range (Without Modulo Bias)
Simple modulo creates bias because the range rarely divides evenly into the max integer:
function secureRandomInt(min, max) { const range = max - min + 1; const maxValid = Math.floor(0x100000000 / range) * range; let value; do { const arr = new Uint32Array(1); crypto.getRandomValues(arr); value = arr[0]; } while (value >= maxValid); // Reject values in the biased range return min + (value % range); } Try the random picker at toolzip.app/tools/random-picker.
