
Random numbers show up everywhere in JavaScript: temporary IDs, animations, randomized UI effects, shuffling lists, lottery-style selection, tokens, UUIDs, encryption keys, and more.
Most of the time, developers reach for one familiar API:
Math.random()
For many everyday tasks, that is perfectly reasonable. But once money, authentication, encryption, keys, tokens, or fairness-sensitive logic enters the picture, Math.random() is the wrong tool. That is where the Web Crypto API, especially crypto.getRandomValues(), becomes important.
The Short Version
Use Math.random() when the result only needs to feel random:
- UI decoration
- animation variation
- casual demos
- non-critical sampling
- throwaway DOM IDs
Use crypto.getRandomValues() when the result must be difficult to predict:
- tokens
- passwords
- invite codes
- lottery or reward systems
- cryptographic keys
- anything involving money, security, or access control
The issue is not simply that Math.random() is “pseudorandom.” crypto.getRandomValues() is also pseudorandom. The real distinction is that Math.random() is not a cryptographically secure pseudorandom number generator, while getRandomValues() is designed for cryptographic use.
Math.random() Can Look Uniform and Still Be Unsafe
A common misunderstanding is that Math.random() is unsafe because its values are not evenly distributed. In practice, a simple sample often looks quite uniform.
The demo code below runs Math.random() many times, places the results into buckets, and counts how many values land in each bucket.
function sampleMathRandomDistribution(total, bucketCount) {
const buckets = Array.from({ length: bucketCount }, () => 0);
for (let i = 0; i < total; i++) {
const index = Math.min(
bucketCount - 1,
Math.floor(Math.random() * bucketCount)
);
buckets[index]++;
}
return buckets;
}
This is useful because it separates two ideas that developers often mix together: statistical distribution and cryptographic unpredictability. A generator can produce values that look evenly distributed while still being predictable if an attacker can recover or infer its internal state.

🎮 Try it live: Open the interactive demo to experience this yourself.
So the problem with Math.random() is not that it always “looks bad.” It is that it was not designed to protect secrets.
Why Predictability Matters
JavaScript engines are free to choose their own Math.random() implementation. V8, the engine used by Chrome and Node.js, has used xorshift-based PRNG logic for Math.random(). V8’s own write-up is explicit: even improved algorithms such as xorshift128+ are not cryptographically secure, and security-sensitive work should use Web Crypto instead.
That matters because normal PRNGs are deterministic. Given the same internal state, they produce the same sequence. If an attacker can infer enough about that state, future values may become easier to predict.
The demo includes a simplified teaching model. It is not V8 source code, but it shows the core risk: once a deterministic generator’s state is known, later values are no longer mysterious.
function makePredictableCache(seed) {
let state = seed >>> 0;
let cache = [];
let index = 0;
function nextRaw() {
state ^= state << 13;
state ^= state >>> 17;
state ^= state << 5;
return state >>> 0;
}
function refill() {
cache = Array.from({ length: 16 }, () => nextRaw());
index = 0;
return cache;
}
function next() {
if (index >= cache.length) {
refill();
}
return cache[index++];
}
refill();
return { next, refill };
}
This model demonstrates why Math.random() should never decide who wins money, who receives a limited reward, or what token grants access to an account.

🎮 Try it live: Open the interactive demo to experience this yourself.
Enter crypto.getRandomValues()
crypto.getRandomValues() fills an integer typed array with cryptographically strong random values.
Its basic syntax is:
crypto.getRandomValues(typedArray)
The array is modified in place and returned. Supported array types include integer typed arrays such as Uint8Array, Uint16Array, Uint32Array, Int32Array, and modern BigInt typed arrays. Floating-point arrays are not allowed.
Here is the practical version from the demo:
function secureUint32() {
return self.crypto.getRandomValues(new Uint32Array(1))[0];
}
function secureBatch(count) {
const values = self.crypto.getRandomValues(new Uint32Array(count));
return Array.from(values);
}
Math.randomValue = function () {
return self.crypto.getRandomValues(new Uint32Array(1))[0];
};
The first function returns one secure unsigned 32-bit integer. The second generates a batch. The helper shows one possible convenience wrapper, although in production code I would usually prefer a clearly named utility like secureRandomUint32() instead of extending Math.

🎮 Try it live: Open the interactive demo to experience this yourself.
What About UUIDs?
For UUIDs, modern browsers provide:
self.crypto.randomUUID()
It generates a cryptographically secure v4 UUID string. As of current MDN documentation, crypto.randomUUID() is widely available and has been broadly supported across browsers since March 2022, but it requires a secure context such as HTTPS.
For older environments, check availability before calling it.
What About crypto.subtle?
The crypto object also exposes crypto.subtle, which contains lower-level cryptographic operations such as:
encrypt()decrypt()sign()verify()digest()generateKey()deriveKey()importKey()exportKey()
These methods return Promises and are intended for real cryptographic workflows. For example, if you are generating actual keys, MDN recommends using dedicated APIs such as SubtleCrypto.generateKey() rather than manually assembling key material from random bytes.
Performance: Should You Always Use Crypto?
Not necessarily.
Math.random() is fast and convenient. For harmless randomness, it is still the right default. If you are randomizing a background particle animation or choosing a casual UI variant, cryptographic randomness adds no meaningful benefit.
crypto.getRandomValues() is designed for stronger unpredictability, not maximum speed. On the frontend, the performance difference usually does not matter. On a hot backend path or high-volume service, choose deliberately.
A practical rule:
Use Math.random() for randomness users cannot exploit.
Use crypto.getRandomValues() for randomness attackers might care about.
Final Takeaway
Math.random() is not broken. It is just often used outside its job description.
For everyday visual randomness, casual demos, and non-critical behavior, it is simple and effective. But for secrets, tokens, keys, rewards, financial outcomes, or anything security-sensitive, use crypto.getRandomValues() or a higher-level crypto API designed for that exact purpose.
Further reading: MDN on getRandomValues(), MDN on randomUUID(), and V8’s Math.random() write-up.
Try It Yourself
Want to see these concepts in action? I’ve created an interactive demo where you can experiment with the code and see real-time results.
Explore more demos from my previous articles in the Demo Gallery.
Happy coding!