Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIniciantecódigo testado

Número aleatório em JavaScript: Math.random na prática

Math.random vai de 0 até quase 1 e nunca chega a 1 — é isso que explica o + 1 da fórmula. Com o off-by-one e o embaralhamento enviesado provados por amostra.

Rodolfo Mori10 min de leitura

Math.random() devolve um decimal entre 0 (que pode sair) e 1 (que nunca sai). Essa assimetria de uma ponta só é a origem de praticamente todo bug de sorteio em JavaScript: é ela que obriga o + 1 na fórmula de intervalo, e é a falta desse + 1 que faz o prêmio maior nunca ser sorteado.

Nesta lição eu não vou pedir para você acreditar em nenhuma dessas afirmações. Cada uma delas está provada por amostragem — mil, cem mil, dez milhões de sorteios contados — porque num assunto onde o resultado muda a cada execução, o único argumento que vale é a distribuição.

Os exemplos são de uma loja: sortear cupom, embaralhar a roleta de prêmios, gerar código de voucher, escolher o ganhador do concurso do mês.

Uma roleta que inclui o zero e deixa o um de fora

Pense numa roleta desenhada de zero até um. A seta pode parar exatamente no zero, mas o ponto marcado como um já pertence à volta seguinte e fica de fora. É uma roleta com a borda esquerda fechada e a direita aberta — a notação técnica para isso é [0, 1).

É esse intervalo que Math.random() entrega. Multiplicar estica a roleta; somar desloca o começo; Math.floor transforma a posição em uma casa inteira. Na primeira amostragem, procure o menor e o maior valor observados: eles são a verificação prática de que o zero participa e o um não.

js
console.log(Math.random());
console.log(Math.random());
console.log(Math.random());

let menor = 1;
let maior = 0;
let deuUm = 0;
for (let i = 0; i < 10_000_000; i += 1) {
  const n = Math.random();
  if (n < menor) menor = n;
  if (n > maior) maior = n;
  if (n === 1) deuUm += 1;
}
console.log('menor valor em 10 milhões:', menor);
console.log('maior valor em 10 milhões:', maior);
console.log('quantas vezes saiu exatamente 1:', deuUm);
0.5973003666774389 0.7653414755884389 0.05895916562094339 menor valor em 10 milhões: 1.0748018031137008e-7 maior valor em 10 milhões: 0.9999996798157813 quantas vezes saiu exatamente 1: 0

Dez milhões de sorteios, nenhum 1. O maior valor que apareceu foi 0.9999996798157813 — perto do teto, e abaixo dele. O menor chegou a 1.07e-7, quase zero. A notação matemática para isso é [0, 1): colchete fechado no zero, parêntese aberto no um.

Guarde essa frase, porque ela explica as duas próximas seções: o teto é inalcançável.

Sortear um inteiro entre dois valores

Para transformar [0, 1) num inteiro de min a max, a fórmula é:

js
const sortear = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

console.log(sortear(1, 6));
console.log(sortear(1, 6));
console.log(Array.from({ length: 12 }, () => sortear(1, 6)).join(' '));
console.log(Array.from({ length: 8 }, () => sortear(10, 20)).join(' '));
4 5 5 3 1 4 6 1 1 6 6 4 1 3 14 12 16 16 20 13 19 18

Vale desmontar a expressão da direita para a esquerda, com min = 1 e max = 6:

  1. max - min + 1 é 6, a quantidade de resultados possíveis.
  2. Math.random() * 6 cai em [0, 6) — chega perto de 6 e não encosta.
  3. Math.floor derruba para o inteiro de baixo: 0, 1, 2, 3, 4 ou 5. O 6 só sairia se o produto chegasse a 6, e ele nunca chega.
  4. + min desloca a faixa de 0..5 para 1..6.

O + 1 do passo 1 e o + min do passo 4 fazem coisas diferentes, e é fácil confundir os dois. O + min move o intervalo; o + 1 alarga o intervalo em uma unidade, compensando exatamente o teto que Math.random nunca alcança. Se Math.floor é assunto novo, os números em JavaScript mostram a diferença entre floor, round, ceil e trunc.

O off-by-one, provado em mil sorteios

Tirar o + 1 parece inofensivo. Não é:

js
const errado = (min, max) => Math.floor(Math.random() * (max - min)) + min;
const certo = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;

function contar(sorteio, vezes = 1000) {
  const contagem = new Map();
  for (let i = 0; i < vezes; i += 1) {
    const n = sorteio(1, 6);
    contagem.set(n, (contagem.get(n) ?? 0) + 1);
  }
  return [...contagem.entries()].sort((a, b) => a[0] - b[0]);
}

console.log('sem o + 1:', JSON.stringify(Object.fromEntries(contar(errado))));
console.log('com o + 1:', JSON.stringify(Object.fromEntries(contar(certo))));
sem o + 1: {"1":203,"2":181,"3":188,"4":219,"5":209} com o + 1: {"1":157,"2":166,"3":172,"4":168,"5":180,"6":157}

Olhe as chaves, não os valores. Na primeira linha o 6 simplesmente não existe: em mil sorteios de um dado, o seis não saiu nenhuma vez. Não é azar, é impossibilidade — a versão sem o + 1 produz no máximo 5, sempre.

Os números de cada faixa mudam a cada execução, e é esperado que mudem. A ausência do 6 não muda nunca.

Sortear um item de um array

Com array, o + 1 não entra: os índices vão de 0 a length - 1, que é justamente o que Math.floor(Math.random() * length) produz.

js
const cupons = ['BEMVINDO10', 'FRETEGRATIS', 'VOLTA15', 'BLACK20'];

const sortear = (lista) => lista[Math.floor(Math.random() * lista.length)];

console.log(sortear(cupons));
console.log(sortear(cupons));
console.log(sortear(cupons));

const contagem = {};
for (let i = 0; i < 40_000; i += 1) {
  const c = sortear(cupons);
  contagem[c] = (contagem[c] ?? 0) + 1;
}
console.log(contagem);
BLACK20 BEMVINDO10 VOLTA15 { BEMVINDO10: 10037, BLACK20: 10140, VOLTA15: 9984, FRETEGRATIS: 9839 }

Quarenta mil sorteios, quatro cupons, dez mil para cada com uma margem de ruído de 1,5%. É essa a cara de uma distribuição uniforme — guarde a imagem, porque daqui a duas seções você vai ver uma que não é.

O lista.length dentro da conta é o que faz a função funcionar para qualquer tamanho de array. Sobre por que o último índice é length - 1, os arrays em JavaScript explicam a numeração a partir do zero.

O sorteio que quebra uma vez a cada três

Quem aprendeu a fórmula do intervalo antes de aprender índice de array acaba misturando as duas, e coloca um + 1 onde ele não cabe:

js
const participantes = [
  { nome: 'Ana', pedido: 8412 },
  { nome: 'Bruno', pedido: 8413 },
  { nome: 'Carla', pedido: 8414 },
];

const indice = Math.floor(Math.random() * participantes.length) + 1;
const ganhador = participantes[indice];

console.log('índice sorteado:', indice);
console.log('Ganhador:', ganhador.nome);

Rodando o mesmo arquivo três vezes seguidas, as duas primeiras passaram:

índice sorteado: 2 Ganhador: Carla

E a terceira:

índice sorteado: 3 file:///private/tmp/loja/sorteio-ganhador.mjs:11 console.log('Ganhador:', ganhador.nome); ^

TypeError: Cannot read properties of undefined (reading ‘nome’) at file:///private/tmp/loja/sorteio-ganhador.mjs:11:35 at ModuleJob.run (node:internal/modules/esm/module_job:439:25) Node.js v24.16.0

Índice 3 num array de três posições devolve undefined, e undefined.nome lança TypeError: Cannot read properties of undefined. Um bug que aparece em um terço das execuções é pior que um bug constante: ele passa no teste, passa na revisão, passa no deploy, e cai no sorteio de sexta à noite.

O tamanho real do estrago só fica visível contando:

js
const participantes = [
  { nome: 'Ana', pedido: 8412 },
  { nome: 'Bruno', pedido: 8413 },
  { nome: 'Carla', pedido: 8414 },
];

const resultado = { Ana: 0, Bruno: 0, Carla: 0, quebrou: 0 };

for (let i = 0; i < 30_000; i += 1) {
  const indice = Math.floor(Math.random() * participantes.length) + 1;
  const ganhador = participantes[indice];
  if (ganhador === undefined) resultado.quebrou += 1;
  else resultado[ganhador.nome] += 1;
}

console.log(resultado);
{ Ana: 0, Bruno: 9942, Carla: 10017, quebrou: 10041 }

Ana nunca ganha. Ela é a primeira da lista, ocupa o índice 0, e o + 1 tornou o zero inalcançável. Um terço dos sorteios estoura e dois terços premiam sempre as mesmas duas pessoas. Se isso fosse um concurso real, seria um problema jurídico, não um bug.

Embaralhar: por que o sort com random é enviesado

O truque mais copiado da internet para embaralhar um array é este:

js
const cupons = ['A', 'B', 'C', 'D', 'E'];

const embaralharComSort = (lista) => [...lista].sort(() => Math.random() - 0.5);

const primeiro = { A: 0, B: 0, C: 0, D: 0, E: 0 };
for (let i = 0; i < 100_000; i += 1) primeiro[embaralharComSort(cupons)[0]] += 1;

console.log('esperado: 20000 para cada');
console.log(primeiro);
esperado: 20000 para cada { A: 32065, B: 12353, C: 16403, D: 21959, E: 17220 }

Cem mil embaralhamentos de cinco cupons. Se o embaralhamento fosse justo, cada letra abriria a lista mais ou menos 20 mil vezes. O A abriu 32 mil e o B abriu 12 mil — quase três vezes mais chance para o primeiro do que para o segundo.

A distribuição completa mostra que o viés não está só na primeira posição:

js
const cupons = ['A', 'B', 'C', 'D', 'E'];
const VEZES = 100_000;

const comSort = (lista) => [...lista].sort(() => Math.random() - 0.5);

function matriz(embaralhar) {
  const conta = cupons.map(() => cupons.map(() => 0));
  for (let i = 0; i < VEZES; i += 1) {
    embaralhar(cupons).forEach((letra, pos) => { conta[cupons.indexOf(letra)][pos] += 1; });
  }
  return conta;
}

console.log(' '.repeat(5) + cupons.map((_, i) => `pos${i}`.padStart(7)).join(' '));
matriz(comSort).forEach((linha, i) => {
  console.log(`  ${cupons[i]}  ` + linha.map((n) => ((n / VEZES) * 100).toFixed(1).padStart(6) + '%').join(' '));
});
pos0 pos1 pos2 pos3 pos4 A 32.3% 16.6% 12.3% 14.0% 24.8% B 12.4% 32.9% 23.8% 23.9% 7.1% C 16.5% 16.3% 28.1% 18.2% 20.9% D 21.9% 23.2% 14.1% 21.8% 18.9% E 16.9% 11.0% 21.7% 22.1% 28.3%

Todas as células deveriam estar em 20%. Nenhuma linha está. O B termina em último em apenas 7,1% dos casos e fica em segundo lugar em 32,9%; o A e o E tendem a ficar nas pontas. O array praticamente não sai do lugar.

O motivo é que sort espera um comparador consistente: se a vem antes de b, tem que vir antes toda vez que a pergunta for repetida. Um comparador que responde no cara ou coroa quebra esse contrato, e o algoritmo de ordenação do V8 não faz todas as comparações possíveis — ele pula pares cuja ordem julga já deduzida. Resultado: o embaralhamento depende do caminho interno do algoritmo, e o array inicial vaza para o final.

Fisher-Yates, o embaralhamento que distribui

O algoritmo correto tem cinco linhas. Ele percorre o array de trás para frente e troca cada posição com uma posição sorteada entre 0 e ela mesma:

js
const cupons = ['A', 'B', 'C', 'D', 'E'];

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

const primeiro = { A: 0, B: 0, C: 0, D: 0, E: 0 };
for (let i = 0; i < 100_000; i += 1) primeiro[embaralhar(cupons)[0]] += 1;

console.log('esperado: 20000 para cada');
console.log(primeiro);
console.log(embaralhar(cupons).join(' '));
esperado: 20000 para cada { A: 20030, B: 20080, C: 20023, D: 19950, E: 19917 }

Vinte mil para cada, com menos de 0,5% de desvio. Repare no i + 1 dentro do laço: ele é o mesmo + 1 da fórmula de intervalo, e serve para que uma posição possa ser sorteada para ficar onde já está. Sem ele o algoritmo também fica enviesado — o item nunca permaneceria no próprio lugar, e permanecer é uma das possibilidades legítimas de um embaralhamento honesto.

Três detalhes de implementação que valem o mesmo que o algoritmo:

  • [...lista] copia antes de mexer. Fisher-Yates troca posições no lugar, e sem a cópia você embaralha o array de quem chamou, silenciosamente.
  • O laço para em i > 0. Sortear uma troca para a posição 0 seria trocá-la com ela mesma — trabalho sem efeito.
  • A troca usa desestruturação, [a[i], a[j]] = [a[j], a[i]], que dispensa a variável temporária.

Sorteio com peso: a roleta de cupons

Nem todo sorteio é uniforme. Uma roleta de prêmios quer 60% de chance para o desconto pequeno e 1% para o teclado:

js
const roleta = [
  { premio: 'R$ 5 de desconto', peso: 60 },
  { premio: 'Frete grátis', peso: 30 },
  { premio: 'R$ 50 de desconto', peso: 9 },
  { premio: 'Teclado mecânico', peso: 1 },
];

const totalPeso = roleta.reduce((s, p) => s + p.peso, 0);

function girar() {
  let n = Math.random() * totalPeso;
  for (const item of roleta) {
    n -= item.peso;
    if (n < 0) return item.premio;
  }
  return roleta[roleta.length - 1].premio;
}

const contagem = {};
for (let i = 0; i < 100_000; i += 1) {
  const p = girar();
  contagem[p] = (contagem[p] ?? 0) + 1;
}

for (const { premio, peso } of roleta) {
  const saiu = ((contagem[premio] ?? 0) / 1000).toFixed(2);
  console.log(`${premio.padEnd(18)} esperado ${String(peso).padStart(2)}%  ·  saiu ${saiu}%`);
}
R$ 5 de desconto esperado 60% · saiu 59.99% Frete grátis esperado 30% · saiu 30.30% R$ 50 de desconto esperado 9% · saiu 8.76% Teclado mecânico esperado 1% · saiu 0.95%

O return fora do laço parece morto e não é: se todos os pesos forem subtraídos e n ainda não tiver ficado negativo por um arredondamento de ponto flutuante na última fatia, é ele que evita um undefined. Uma linha de seguro para um caso que acontece uma vez em muitos milhões — que é exatamente a frequência com que uma roleta de produção é girada.

Quando Math.random não serve: crypto

Math.random é rápida e bem distribuída, mas previsível: o estado interno do gerador pode ser reconstruído a partir de algumas saídas observadas. Para qualquer valor que um atacante ganharia adivinhando — token de sessão, código de recuperação de senha, link de convite — use a API crypto, que já é global no Node e no navegador:

js
console.log(crypto.randomUUID());
console.log(crypto.randomUUID().length);

const bytes = crypto.getRandomValues(new Uint8Array(16));
console.log(Buffer.from(bytes).toString('hex'));

const ALFABETO = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
function codigoCupom(tamanho = 8) {
  const valores = crypto.getRandomValues(new Uint8Array(tamanho));
  return [...valores].map((v) => ALFABETO[v % ALFABETO.length]).join('');
}
console.log(codigoCupom());
console.log(codigoCupom(12));
3a78c2a1-96e1-4556-9104-5bbdc38a6d46 36 eb2653d658e86a54cda9b4d809593dbf VD4WZF6E 7T4VTEGEMZMR

randomUUID resolve identificador — 36 caracteres, formato padronizado, colisão improvável o bastante para ser ignorada. getRandomValues preenche um TypedArray com bytes imprevisíveis, e é a base de qualquer gerador de código seu. O alfabeto do exemplo não tem I, O, 0 nem 1, de propósito: cupom é lido em voz alta ao telefone.

getRandomValues só aceita TypedArray de inteiros. Passar um array comum falha:

js
const bytes = crypto.getRandomValues(new Array(16));

console.log(bytes);
node:internal/crypto/random:327 throw lazyDOMException( ^

DOMException [TypeMismatchError]: The data argument must be an integer-type TypedArray at Crypto.getRandomValues (node:internal/crypto/random:327:11) at Crypto.getRandomValues (node:internal/crypto/webcrypto:1757:10) at file:///private/tmp/loja/cripto-erro.mjs:1:22 Node.js v24.16.0

A segurança tem preço, e ele é mensurável:

js
const N = 200_000;

let t = performance.now();
for (let i = 0; i < N; i += 1) Math.random();
const tempoMath = performance.now() - t;

const buffer = new Uint32Array(1);
t = performance.now();
for (let i = 0; i < N; i += 1) crypto.getRandomValues(buffer);
const tempoCripto = performance.now() - t;

console.log(`Math.random()      ${N} chamadas: ${tempoMath.toFixed(1)} ms`);
console.log(`getRandomValues()  ${N} chamadas: ${tempoCripto.toFixed(1)} ms`);
console.log(`razão: ${(tempoCripto / tempoMath).toFixed(0)}x`);
Math.random() 200000 chamadas: 2.0 ms getRandomValues() 200000 chamadas: 110.4 ms razão: 56x

Cinquenta e seis vezes mais lento, na minha máquina, no Node 24.16.0. Isso não é motivo para evitar crypto — 110 ms para duzentos mil tokens é irrelevante em qualquer aplicação real. É motivo para não trocar Math.random por crypto dentro de um laço de animação ou numa simulação de milhões de iterações, onde a imprevisibilidade não compra nada.

Determinismo: Math.random não aceita semente

Em outras linguagens você fixa uma semente e o gerador repete a mesma sequência. Em JavaScript, não existe essa API — e argumentos passados para Math.random são simplesmente ignorados:

js
console.log(Math.random(42));
console.log(Math.random(42));
console.log(Math.random.length);
0.18159426181298532 0.7485853199328232 0

Duas chamadas com o mesmo “argumento”, dois resultados diferentes. Math.random.length é 0: a função declara zero parâmetros, então o 42 não chegou a lugar nenhum.

Quando o teste precisa de resultado repetível, injete o gerador em vez de chamar Math.random direto. Um mulberry32 cabe em seis linhas e é suficiente para teste e para simulação:

js
function mulberry32(semente) {
  let a = semente >>> 0;
  return function () {
    a = (a + 0x6d2b79f5) >>> 0;
    let t = a;
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const random = mulberry32(2026);
console.log([random(), random(), random()]);

const outro = mulberry32(2026);
console.log([outro(), outro(), outro()]);

const sortearCom = (random, min, max) => Math.floor(random() * (max - min + 1)) + min;
const dados = mulberry32(7);
console.log(Array.from({ length: 10 }, () => sortearCom(dados, 1, 6)));
[ 0.45540769933722913, 0.30849614599719644, 0.6611574492417276 ] [ 0.45540769933722913, 0.30849614599719644, 0.6611574492417276 ] [ 1, 1, 6, 5, 4, 3, 3, 2, 4, 5 ]

Duas instâncias com a mesma semente produzem exatamente a mesma sequência. A função que sorteia passa a receber o gerador como parâmetro — em produção você passa Math.random, no teste passa mulberry32(7), e a asserção volta a ser uma asserção.

Regras práticas

você quer escreva cuidado
inteiro de min a max Math.floor(Math.random() * (max - min + 1)) + min sem o + 1 o max nunca sai
item de um array lista[Math.floor(Math.random() * lista.length)] aqui não entra + 1
embaralhar Fisher-Yates sort(() => Math.random() - 0.5) é enviesado
sorteio com chances diferentes soma de pesos e subtração mantenha o return de segurança
token, senha, link de convite crypto.getRandomValues Math.random é previsível
identificador único crypto.randomUUID() 36 caracteres, já vem pronto
resultado repetível no teste gerador com semente injetado Math.random ignora argumentos

Antes de subir qualquer sorteio para produção, rode ele mil vezes num laço e imprima a contagem. São quatro linhas de código descartável, e elas mostram o off-by-one e o viés que nenhuma leitura de código mostra. O guia completo de JavaScript situa esta lição no roteiro, e o resto do que se publica por aqui está na categoria de front-end.

Prefere aprender em vídeo?

Tem uma aula sobre este assunto no nosso canal.

Ver todos os vídeos do canal
  • math random
  • sorteio
  • embaralhar
  • fisher-yates
  • crypto

Perguntas frequentes

Por que a fórmula do sorteio tem um + 1?
Porque Math.random() nunca devolve 1. Multiplicando por (max - min), o maior produto possível fica logo abaixo de (max - min), e Math.floor derruba para (max - min - 1). O + 1 amplia a faixa em um, e é o que permite o valor máximo sair. Sem ele, o topo do intervalo nunca aparece.
Posso embaralhar com array.sort(() => Math.random() - 0.5)?
Não, se a ordem importar. O comparador é inconsistente e o algoritmo de ordenação do V8 não visita todos os pares, então algumas permutações saem muito mais que outras. Em 100 mil embaralhamentos de cinco itens, o primeiro elemento ficou na primeira posição em 32% das vezes, e não em 20%. Use Fisher-Yates.
Math.random serve para gerar senha ou token?
Não. Ela é rápida e uniforme, mas previsível: o estado interno do gerador pode ser reconstruído a partir de algumas saídas. Para qualquer coisa que um atacante ganharia adivinhando — token, senha, código de recuperação — use crypto.getRandomValues ou crypto.randomUUID.
Como fixar uma semente para o teste dar sempre o mesmo resultado?
Math.random não aceita semente, e nenhum argumento passado para ela faz efeito. Ou você injeta um gerador próprio com semente na função que sorteia, ou usa o mock de aleatoriedade do seu runner de testes. A terceira via, node --random-seed=42, muda o processo inteiro e serve para reproduzir uma investigação, não para rodar em produção.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.

Fontes consultadas

  1. MDN — Math.random() — developer.mozilla.org
  2. MDN — Crypto.getRandomValues() — developer.mozilla.org
  3. MDN — Crypto.randomUUID() — developer.mozilla.org

Continue por aqui