Skip to content
JavaScript js numbers 4 min read

Generating Random Numbers

Randomness powers everything from shuffled playlists and dice rolls to particle systems and procedural generation. JavaScript’s everyday tool for this is Math.random(), which returns a floating-point number — but turning that single primitive into a random integer in a range, a random array element, or a fair shuffle takes a few well-known recipes. This page collects those recipes, explains the off-by-one traps that bite beginners, and shows when to reach for the cryptographically secure API instead.

The building block: Math.random

Math.random() returns a pseudo-random float in the half-open interval [0, 1) — that is, zero is possible but one never is. Every other recipe on this page is built by scaling and shifting this value.

console.log(Math.random()); // e.g. 0.4729103857...
console.log(Math.random()); // a different value each call

Because the range is [0, 1), multiplying by n gives a float in [0, n), which is exactly the shape you want before flooring into bucket indices.

Math.random() is not cryptographically secure and must never be used for passwords, tokens, session IDs, or anything an attacker should not predict. See the security section below.

Random float in a range

To get a float in [min, max), scale the [0, 1) value by the span and add the offset.

function randomFloat(min, max) {
  return Math.random() * (max - min) + min;
}

console.log(randomFloat(1, 5));    // e.g. 3.871...
console.log(randomFloat(-1, 1));   // e.g. -0.244...

The result includes min but excludes max, matching the convention of Math.random() itself.

Random integer in a range

The most common need is a whole number. The classic inclusive formula uses Math.floor on a scaled value. To make both min and max reachable, the span is (max - min + 1).

function randomInt(min, max) {
  // inclusive of both min and max
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

console.log(randomInt(1, 6));   // a dice roll: 1..6
console.log(randomInt(0, 9));   // a single digit: 0..9
console.log(randomInt(-5, 5));  // negatives work too

Output:

4
7
-2

The + 1 is the part people forget. Without it the maximum value is unreachable, producing a subtle bias against the top of the range.

GoalExpressionRange
FloatMath.random()[0, 1)
Float in rangeMath.random() * (max - min) + min[min, max)
Integer in rangeMath.floor(Math.random() * (max - min + 1)) + min[min, max]
Coin flipMath.random() < 0.5true / false

Picking a random array element

Reuse the integer recipe to index into an array. The valid index range is 0 to length - 1.

function randomItem(arr) {
  return arr[Math.floor(Math.random() * arr.length)];
}

const colors = ["red", "green", "blue", "purple"];
console.log(randomItem(colors)); // e.g. "blue"

Here Math.floor(Math.random() * arr.length) already produces 0..length-1 because the upper bound is exclusive — no + 1 needed, and no risk of an out-of-bounds undefined.

Shuffling with Fisher-Yates

To randomize the order of an array, do not sort with Math.random() - 0.5. That trick is biased and produces uneven distributions because comparison sorts call the comparator inconsistently. The correct algorithm is Fisher-Yates, which walks from the end and swaps each element with a randomly chosen earlier (or current) one.

function shuffle(arr) {
  // returns a new shuffled array, leaving the original intact
  const result = [...arr];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

console.log(shuffle([1, 2, 3, 4, 5]));

Output:

[ 3, 1, 5, 2, 4 ]

Each of the n! orderings is equally likely, and the loop runs in linear time. Spreading into a copy keeps the function pure; drop the copy and operate on arr directly for an in-place shuffle.

Avoid arr.sort(() => Math.random() - 0.5). It looks clever but the distribution is provably skewed and depends on the engine’s sort implementation. Always use Fisher-Yates for fairness.

Cryptographically secure randomness

When unpredictability matters — tokens, salts, game-fairness guarantees, anything security-sensitive — use the Web Crypto API’s crypto.getRandomValues(). It fills a typed array with values from a CSPRNG and is available in browsers and modern Node (via the global crypto).

// a secure integer in [0, max) without modulo bias for power-of-two ranges
function secureRandomInt(max) {
  const arr = new Uint32Array(1);
  crypto.getRandomValues(arr);
  return arr[0] % max;
}

// a secure hex token
function token(bytes = 16) {
  const arr = new Uint8Array(bytes);
  crypto.getRandomValues(arr);
  return [...arr].map(b => b.toString(16).padStart(2, "0")).join("");
}

console.log(secureRandomInt(100)); // e.g. 73
console.log(token());              // e.g. "9f2a1c...e4"

For a ready-made UUID, crypto.randomUUID() returns an RFC 4122 v4 string with no manual byte juggling. Note that the simple % max above introduces slight modulo bias for ranges that do not divide evenly into 2^32; for strict uniformity, reject-and-retry values in the unfair tail.

Best Practices

  • Use Math.random() for visuals, games, and simulations; never for security.
  • Memorize the inclusive integer formula Math.floor(Math.random() * (max - min + 1)) + min and remember the + 1.
  • Index arrays with Math.floor(Math.random() * arr.length) — no + 1, since indices stop at length - 1.
  • Shuffle with Fisher-Yates, not sort(() => Math.random() - 0.5).
  • Reach for crypto.getRandomValues() or crypto.randomUUID() whenever values must be unpredictable.
  • Wrap your recipes in named helper functions (randomInt, randomItem, shuffle) so intent is clear and the off-by-one logic lives in one place.
Last updated June 1, 2026
Was this helpful?