Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. How to generate a random number in a range in JavaScript
Learn

How to generate a random number in a range in JavaScript

To get a random number in JavaScript, the Math.round version of the formula gives 1 and 6 half the odds of the rest. Measured over 600,000 rolls.

By Max Arthur
Co-Founder & Content Marketer·September 27, 2026·9 Min read
White text reading 'Random numbers in JavaScript' on a dark purple dotted background

The formula, and it is worth copying exactly:

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

randomInt(1, 6);   // 1 to 6, inclusive

Three details make it correct: Math.floor rather than Math.round, the + 1 inside the brackets, and the + min outside them. Change any one and the distribution stops being even.

That is not a theoretical claim. Here is each mistake, measured over hundreds of thousands of rolls in Node v24.14.1.

Why Math.round is wrong

Swap Math.floor for Math.round and drop the + 1, which is the version that circulates most:

Math.round(Math.random() * (max - min)) + min;

600,000 rolls of a six-sided die:

Face Correct formula With Math.round
1 100,106 59,712
2 100,155 120,023
3 99,788 120,111
4 99,818 119,884
5 99,724 120,044
6 100,409 60,226

1 and 6 come up half as often as everything else. Not slightly less: half.

The reason is that rounding maps a half-width band to the endpoints. Values from 0 to 0.5 round to 0, and 0.5 to 1 round to 1, so the first and last buckets are half the size of the interior ones, which collect a full unit each.

Math.floor gives every bucket the same width, which is why it is the right function even though "round" sounds like the natural choice for producing an integer.

Why the + 1 matters

Keep Math.floor but drop the + 1:

Math.floor(Math.random() * (max - min)) + min;

60,000 rolls:

{ 1: 12043, 2: 12182, 3: 11909, 4: 11898, 5: 11968 }

Six never appears. Not rarely, never.

Math.random() returns a value in [0, 1), meaning zero is possible and one is not. Multiplying by max - min gives [0, 5), and flooring that can only produce 0 through 4, so adding min gives 1 through 5. The + 1 widens the range so the top value is reachable.

This is the mistake that survives testing, because the code looks right and produces plausible numbers. It fails only on the boundary, and a quick manual check rarely lands there.

What Math.random actually gives you

Math.random();   // a float in [0, 1)

Inclusive of 0, exclusive of 1. Every formula on this page is arithmetic applied to that one guarantee, and remembering the half-open interval is what makes the + 1 obvious rather than magic.

For a random decimal rather than an integer, there is no flooring and no + 1:

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

That returns a value in [min, max), with the maximum excluded for the same reason.

The variations you will need

// integer between min and max, inclusive
Math.floor(Math.random() * (max - min + 1)) + min;

// a random array element
arr[Math.floor(Math.random() * arr.length)];

// a coin flip
Math.random() < 0.5;

// something that happens 30% of the time
Math.random() < 0.3;

The array one is worth reading closely, because it uses arr.length with no + 1. Indices run from 0 to length - 1, and Math.random() * length floored gives exactly that range. Adding one here would produce an out-of-bounds index roughly one time in every length calls, which is the kind of bug that appears in production and not in testing.

Shuffling an array

The naive shuffle is one of the most-copied wrong answers in JavaScript:

arr.sort(() => Math.random() - 0.5);   // do not use

It looks elegant and does not produce an even distribution. The comparator is inconsistent, meaning it can give different answers for the same pair, and sort algorithms assume consistency. The result is biased in a way that depends on the engine's sort implementation and the array's length.

Fisher-Yates is the correct algorithm and is only three lines:

function shuffle(arr) {
  const a = [...arr];
  for (let i = a.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

Walk backwards, and at each position swap with a random earlier index including itself. Every permutation becomes equally likely, and the proof is straightforward enough to trust.

Note the [...arr] copy at the top. Shuffling in place is a common source of surprise when the array came from somewhere else, in the same way sort mutating its input is.

When Math.random is not good enough

Math.random() is fast, evenly distributed, and not cryptographically secure. Its output is predictable given enough samples, and the specification explicitly declines to guarantee otherwise.

That is fine for dice, shuffling a playlist, or picking a background colour. It is not fine for anything that must not be guessable:

  • Password reset tokens
  • Session identifiers
  • Anything used as a secret

For those, use the crypto API, which is available in browsers and in Node:

crypto.randomUUID();
// "f5311bd0-5bc7-..."
const buf = new Uint32Array(1);
crypto.getRandomValues(buf);
const n = buf[0];

crypto.randomUUID() covers most identifier needs in one call. getRandomValues fills a typed array with cryptographically strong values when you need a number rather than a UUID.

There is a subtlety if you use getRandomValues to produce a range. Taking the value modulo your range size reintroduces bias, for the same reason the Math.round version above is biased: the buckets are not equal unless the range divides evenly. Rejection sampling, where you discard values in the uneven tail and draw again, is the correct fix.

Seeded and repeatable randomness

Math.random() cannot be seeded, so you cannot reproduce a sequence. That matters for tests, for procedural generation, and for anything where a bug needs to be reproduced.

The usual answer is a small seedable generator such as mulberry32, which is a handful of lines and produces a deterministic sequence from a starting number. In tests, the simpler option is to stub Math.random entirely so the values are fixed and the assertion is about your logic rather than about chance.

Either way, a test that calls the real Math.random() and asserts on the result is a test that will eventually fail for no reason.

Weighted choices

When outcomes should not be equally likely, the usual approach is a running total:

function weightedPick(items) {
  const total = items.reduce((sum, i) => sum + i.weight, 0);
  let r = Math.random() * total;
  for (const item of items) {
    if ((r -= item.weight) < 0) return item.value;
  }
}

weightedPick([
  { value: "common", weight: 70 },
  { value: "rare", weight: 25 },
  { value: "legendary", weight: 5 },
]);

Pick a point along the combined weight, then walk the list subtracting until you pass it. The weights do not need to add to 100 or to anything in particular, which makes them easy to tune.

The simpler version, building an array with each value repeated by its weight, works and allocates a hundred entries to make one choice. Fine for three outcomes, wasteful for many.

Random values that are not numbers

Most "random" needs are really "pick one of these":

const pick = (arr) => arr[Math.floor(Math.random() * arr.length)];

pick(["rock", "paper", "scissors"]);

One helper covers picking a colour, a placeholder avatar, a sample message or a test fixture. Note again the absence of + 1, since arr.length is already one past the last index.

For several distinct items, shuffle and take a slice rather than picking repeatedly:

shuffle(arr).slice(0, 3);

Picking three times independently can return the same item twice, which is almost never what "pick three" means. Shuffling first makes them distinct by construction.

Testing code that uses randomness

A test that calls the real Math.random() and asserts on the output will eventually fail for no reason. Two ways round it.

Stub it, so the value is fixed and the test is about your logic:

const spy = jest.spyOn(Math, "random").mockReturnValue(0.5);

Assert on properties rather than values. For a range function, run it many times and check every result falls inside the range, which is what the distribution tables on this page are doing:

for (let i = 0; i < 10000; i++) {
  const n = randomInt(1, 6);
  expect(n).toBeGreaterThanOrEqual(1);
  expect(n).toBeLessThanOrEqual(6);
}

That catches the missing + 1 bug, since six would never appear, and it does not depend on any particular value coming up.

Random strings and ids

A frequent follow-on question, and the answer depends entirely on what the id is for.

For anything user-visible but not secret, such as a temporary key for a list row, crypto.randomUUID() is one call and needs no helper.

For a short human-readable code, pick from an alphabet:

const alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
const code = Array.from(
  { length: 6 },
  () => alphabet[Math.floor(Math.random() * alphabet.length)],
).join("");

Note what is missing from that alphabet: I, O, 0, 1 and L, which people confuse when reading a code aloud or copying it from a screen. That omission matters more than the randomness for a code someone has to type.

For anything security-relevant, use crypto.getRandomValues and remember the modulo bias point above. A short code built from Math.random() is guessable, which is fine for a game room and not for a password reset.

Three common mistakes

Using Math.round. The endpoints get half the probability, measured above at 60,000 against 120,000. Use Math.floor with the + 1.

Forgetting the + 1. The maximum becomes unreachable, and nothing about the output looks wrong until someone notices six never comes up.

Adding + 1 when indexing an array. arr.length is already one more than the largest valid index, so the correct form has no + 1 and adding one produces occasional undefined.

Where the randomness comes from

Math.random() is a pseudo-random number generator: a deterministic algorithm producing a sequence that passes statistical tests for randomness. Engines seed it from an unpredictable source at startup, so two page loads give different sequences, and the sequence itself is entirely determined by that seed.

That is what "not cryptographically secure" means in practice. Given enough consecutive outputs, the internal state can be reconstructed and every future value predicted. For dice that is irrelevant. For a password reset token it is the whole problem.

The other consequence is that you cannot reproduce a sequence, because the seed is not exposed anywhere. That is why a seeded generator has to be written in userland rather than configured, and why test code stubs Math.random rather than seeding it.

Quick reference

Math.random()                                   // [0, 1)
Math.floor(Math.random() * (max - min + 1)) + min   // integer, inclusive
Math.random() * (max - min) + min                   // float, max exclusive
arr[Math.floor(Math.random() * arr.length)]         // random element
Math.random() < 0.5                                 // coin flip
crypto.randomUUID()                                 // secure identifier
crypto.getRandomValues(new Uint32Array(1))          // secure number

Want to roll a few hundred thousand dice and check the distribution yourself? Start with the JavaScript track, or read Big O notation explained for why the shuffle above beats sorting by a random comparator.

Keep reading

More from the blog

White text reading 'How range() works in Python' on a dark purple dotted background
October 12, 2026·8 min readLearn

range() in Python: start, stop, step, and why it is lazy

range(1000000) uses 24 bytes. A list of a thousand numbers uses 4,028. Here is what range actually is, measured.

Read more
White text reading 'How to convert a string to a number in JavaScript' on a dark purple dotted background
October 3, 2026·9 min readLearn

How to convert a string to a number in JavaScript

Converting a string to a number in JavaScript: an empty string is 0 with Number and NaN with parseInt. What four methods do to awkward input.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons