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.
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.
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);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 é:
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(' '));Vale desmontar a expressão da direita para a esquerda, com min = 1 e
max = 6:
max - min + 1é6, a quantidade de resultados possíveis.Math.random() * 6cai em[0, 6)— chega perto de 6 e não encosta.Math.floorderruba para o inteiro de baixo:0,1,2,3,4ou5. O6só sairia se o produto chegasse a6, e ele nunca chega.+ mindesloca a faixa de0..5para1..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 é:
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))));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.
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);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:
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:
E a terceira:
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:
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 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:
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);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:
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(' '));
});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:
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(' '));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ção0seria 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:
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}%`);
}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:
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));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:
const bytes = crypto.getRandomValues(new Array(16));
console.log(bytes);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:
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`);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:
console.log(Math.random(42));
console.log(Math.random(42));
console.log(Math.random.length);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:
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)));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.
Perguntas frequentes
Por que a fórmula do sorteio tem um + 1?
Posso embaralhar com array.sort(() => Math.random() - 0.5)?
Math.random serve para gerar senha ou token?
Como fixar uma semente para o teste dar sempre o mesmo resultado?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
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
- MDN — Math.random() — developer.mozilla.org
- MDN — Crypto.getRandomValues() — developer.mozilla.org
- MDN — Crypto.randomUUID() — developer.mozilla.org



