Picking a random element from an array comes up constantly: rolling dice, shuffling prompts, choosing a color. The whole job is one line of JavaScript, but the interesting part is proving it behaves — that the picks are spread across the array and never fall off either end. This tutorial builds that one-liner as an ES module, backs it with a Node test suite that pins down the boundary behavior, adds a distribution demo, and then draws the results on a browser canvas so you can see the spread.
Prerequisites
- Node.js 18 or later (validated with Node 26.4.0)
- Python 3 for the local static server (
python3ships with the Xcode Command Line Tools on macOS) make(pre-installed on macOS and most Linux distributions)- A Unix-like terminal (macOS, Linux, or WSL on Windows)
No third-party npm packages are used; the tests run on Node’s built-in runner.
Step 1: Create the project and its package file
Make the project folder and add a package.json. The one field that matters here is "type": "module", which tells Node to treat .js files as ES modules so import/export work without a build step.
Create the file
mkdir -p js-rolldice-101
cd js-rolldice-101
touch package.json
Add the code: js-rolldice-101/package.json
{
"name": "js-rolldice-101",
"version": "1.0.0",
"description": "Select a random item from a JavaScript array (Node and browser).",
"type": "module",
"scripts": {
"demo": "node demo.js",
"test": "node --test"
},
"license": "MIT"
}
Detailed breakdown
"type": "module"switches the package to ES modules. Without it, animportstatement in a.jsfile throwsSyntaxError: Cannot use import statement outside a module.- The
demoandtestscripts give younpm run demoandnpm testas aliases for the commands used later; the Makefile in Step 6 wraps the same two commands. - There are no
dependencies— the module, its tests, and the browser code all use built-in APIs only.
Step 2: Write the rollDice function
Create the file
touch rolldice.js
Add the code: js-rolldice-101/rolldice.js
// File: rolldice.js
// Return a random element from an array by generating a random 0-based index.
export const rollDice = (list) => list[Math.floor(Math.random() * list.length)];
Detailed breakdown
Math.random()returns a float in the range[0, 1)— zero is possible, but the value is always strictly less than one.- Multiplying by
list.lengthscales that into[0, list.length), so for a 5-element array the value lands somewhere from0up to (but never reaching)5. Math.floor()truncates to an integer, giving a valid zero-based index from0tolist.length - 1. Because the random value never reacheslist.length, the index can never be out of bounds. Step 3’s tests pin down both ends of that range.export constmakes the function importable from the demo, the tests, and the browser app that follow.
Step 3: Roll the array and inspect the distribution
A quick way to sanity-check a random picker is to run it many times and count how often each item comes up. If the picks are evenly spread, every item should appear with roughly equal frequency.
Create the file
touch demo.js
Add the code: js-rolldice-101/demo.js
// File: demo.js
// Roll a source array many times and count how often each item comes up, so you
// can eyeball whether the distribution looks roughly even.
import { rollDice } from "./rolldice.js";
const source = ["A", "B", "C", "D", "E"];
const LIMIT = 100;
const rolls = Array.from({ length: LIMIT }, () => rollDice(source));
console.log(rolls);
const occurrences = rolls.reduce((acc, item) => {
acc[item] = (acc[item] || 0) + 1;
return acc;
}, {});
console.log(occurrences);
Detailed breakdown
Array.from({ length: LIMIT }, () => rollDice(source))builds a 100-element array by callingrollDiceonce per slot — a compact way to collect many samples.- The
reduceaccumulates a tally object, incrementingacc[item]for each roll (starting from0the first time an item is seen). The counts always sum toLIMIT. - Both
console.logcalls print to the terminal: first the raw roll sequence, then the summary tally.
Run it:
node demo.js
The output varies on every run because the rolls are random. One run produced:
[
'A', 'A', 'D', 'E', 'D', 'B', 'E', 'E', 'E', 'A', 'D',
'C', 'D', 'A', 'C', 'C', 'B', 'A', 'E', 'A', 'E', 'D',
'C', 'A', 'A', 'C', 'E', 'A', 'C', 'D', 'A', 'B', 'E',
'C', 'B', 'E', 'E', 'B', 'B', 'A', 'C', 'B', 'D', 'B',
'A', 'C', 'C', 'C', 'A', 'D', 'C', 'B', 'A', 'B', 'E',
'E', 'E', 'E', 'B', 'B', 'D', 'C', 'E', 'B', 'E', 'E',
'A', 'D', 'A', 'E', 'B', 'A', 'C', 'D', 'E', 'C', 'B',
'C', 'A', 'A', 'B', 'C', 'C', 'E', 'D', 'E', 'C', 'B',
'B', 'C', 'E', 'D', 'E', 'A', 'C', 'C', 'B', 'D', 'B',
'C'
]
{ A: 20, D: 14, E: 23, B: 20, C: 23 }
With 5 items and 100 rolls, a perfectly even split would be 20 each. You will rarely see exactly that, but most runs land close, and every item should appear at least once. The next step turns those expectations into automated tests.
Step 4: Pin the behavior with tests
The demo shows the spread but asserts nothing. Node’s built-in test runner (node --test, stable since Node 20) covers the parts of rollDice that must always hold, with no test framework to install.
Create the file
mkdir -p test
touch test/rolldice.test.js
Add the code: js-rolldice-101/test/rolldice.test.js
// File: test/rolldice.test.js
// Runs with the built-in Node test runner: `node --test`.
import { test } from "node:test";
import assert from "node:assert/strict";
import { rollDice } from "../rolldice.js";
test("returns an element that is a member of the list", () => {
const source = ["A", "B", "C", "D", "E"];
for (let i = 0; i < 1000; i++) {
assert.ok(source.includes(rollDice(source)));
}
});
test("a single-element list always returns that element", () => {
assert.equal(rollDice(["only"]), "only");
});
test("the low bound of Math.random selects the first item", () => {
const original = Math.random;
Math.random = () => 0;
try {
assert.equal(rollDice(["first", "second", "third"]), "first");
} finally {
Math.random = original;
}
});
test("a value just below 1 selects the last item", () => {
const original = Math.random;
Math.random = () => 0.999999;
try {
assert.equal(rollDice(["first", "second", "third"]), "third");
} finally {
Math.random = original;
}
});
test("every item appears at least once over many rolls", () => {
const source = ["A", "B", "C", "D", "E"];
const seen = new Set(Array.from({ length: 500 }, () => rollDice(source)));
assert.equal(seen.size, source.length);
});
Detailed breakdown
- The first test is a property check: across 1000 rolls, every result must be a member of the input array. It never inspects a specific value, only the invariant.
- The two
Math.randomtests replace the global with a stub to make the pick deterministic. Stubbing it to0forces index0(the first item); stubbing it to0.999999forces the last index, which proves the[0, 1)range never overflows to an out-of-bounds index. Each restores the original in afinallyblock so a failed assertion cannot leak the stub into later tests. - The last test collects 500 rolls into a
Setand asserts all five items showed up. With five items this is statistically near-certain; if it ever flaked, the source array or the function would be the real suspect.
Run the suite:
node --test
✔ returns an element that is a member of the list
✔ a single-element list always returns that element
✔ the low bound of Math.random selects the first item
✔ a value just below 1 selects the last item
✔ every item appears at least once over many rolls
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
Step 5: Visualize the spread in the browser
Numbers confirm the distribution; a grid makes it obvious. The browser version fills a 10x10 grid with random color picks and paints each cell on a canvas.
Start with the page. Create index.html:
Create the file
touch index.html
Add the code: js-rolldice-101/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>js-rolldice-101</title>
<link rel="stylesheet" href="./app.css" />
</head>
<body>
<canvas id="canvas" width="300" height="300"></canvas>
<script type="module" src="./app.js"></script>
</body>
</html>
Detailed breakdown
- The
widthandheightattributes set the canvas drawing buffer to 300x300 pixels; the CSS in the next file controls how large it appears on screen. <script type="module">loadsapp.jsas an ES module so itsimportofrolldice.jsworks in the browser, matching the"type": "module"setting from Step 1.<canvas>needs an explicit closing tag; it is not a void element and cannot self-close.
Add the stylesheet to center the canvas. Create app.css:
Create the file
touch app.css
Add the code: js-rolldice-101/app.css
/* File: app.css
* Center the canvas in the middle of the viewport.
*/
canvas {
padding: 0;
margin: auto;
display: block;
width: 400px;
height: 400px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
Detailed breakdown
position: absolutewith all four offsets set to0plusmargin: autocenters the canvas both horizontally and vertically.- The
width/heighthere (400px) are the on-screen display size. The browser scales the 300x300 drawing buffer up to fill it, so the grid stays crisp and centered.
Now the drawing code. Create app.js:
Create the file
touch app.js
Add the code: js-rolldice-101/app.js
// File: app.js
// Fill a 10x10 grid with random color picks and draw it on the canvas.
import { rollDice } from "./rolldice.js";
const canvas = document.getElementById("canvas");
const SCREEN_SIZE = 300; // must match the canvas width/height attributes
const DIM = 10; // grid is DIM x DIM cells
const CELL_SIZE = SCREEN_SIZE / DIM;
const BORDER = 1;
const NEON_PINK = "#FF10F0";
const SOURCE = ["white", NEON_PINK, "#444444"];
// One random color per grid cell.
const cells = Array.from({ length: DIM * DIM }, () => rollDice(SOURCE));
console.log(cells);
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.clearRect(0, 0, SCREEN_SIZE, SCREEN_SIZE);
ctx.fillStyle = "black";
ctx.fillRect(0, 0, SCREEN_SIZE, SCREEN_SIZE);
let cursor = 0;
for (let i = 0; i < DIM; i++) {
for (let j = 0; j < DIM; j++) {
ctx.fillStyle = cells[cursor++];
ctx.fillRect(
i * CELL_SIZE + BORDER,
j * CELL_SIZE + BORDER,
CELL_SIZE - BORDER * 2,
CELL_SIZE - BORDER * 2,
);
}
}
}
Detailed breakdown
SOURCEis the arrayrollDicepicks from: white, neon pink, and dark gray.DIM * DIMis 100, socellsholds one random color per grid cell.SCREEN_SIZE / DIMgives each cell a 30-pixel slot in the drawing buffer;BORDERshrinks each painted square by one pixel on every side so the black background shows through as gridlines.- The nested loops walk the grid column by column, pulling the next color off
cellswithcursor++and filling that cell. console.log(cells)prints the picks to the browser DevTools console, the browser counterpart to the terminal output in Step 3.
Step 6: Serve the page and drive it with a Makefile
Browsers block ES-module imports over the file:// protocol, so open the page through a local web server rather than double-clicking the file. A short Makefile wraps the demo, the tests, and the server, and prints a help screen by default.
Create the file
touch Makefile
Add the code: js-rolldice-101/Makefile
.DEFAULT_GOAL := help
SHELL := /bin/bash
PORT ?= 8000
.PHONY: help demo test serve
help: ## Show this help screen
@echo "Targets:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \
| sort \
| awk 'BEGIN {FS = ":.*?## "}; {printf " %-8s %s\n", $$1, $$2}'
demo: ## Roll the array 100 times and print the distribution
@node demo.js
test: ## Run the Node test suite (node --test)
@node --test
serve: ## Serve the browser visualization at http://localhost:$(PORT)/
@echo "Serving on http://localhost:$(PORT)/ (Ctrl-C to stop)"
@python3 -m http.server $(PORT)
Detailed breakdown
.DEFAULT_GOAL := helpmakes a baremakeprint the help screen instead of running the first target, so the project documents itself.PORT ?= 8000sets a default port that a caller can override, e.g.make serve PORT=9000.serveruns Python’s built-in static file server from the project root, soindex.html,app.js, androlldice.jsare all reachable athttp://localhost:8000/.
Confirm the help screen:
make
Targets:
demo Roll the array 100 times and print the distribution
help Show this help screen
serve Serve the browser visualization at http://localhost:$(PORT)/
test Run the Node test suite (node --test)
Start the server, then open the page in a browser:
make serve
Visit http://localhost:8000/ in Chrome (or any browser) and reload a few times. Each load repaints the grid with a fresh set of random picks, and the three colors stay roughly balanced across the 100 cells. Press Ctrl-C in the terminal to stop the server.
Recap
The pick itself is list[Math.floor(Math.random() * list.length)]. The work around it is what makes it trustworthy: "type": "module" to use import/export cleanly, a node --test suite that stubs Math.random to prove both index bounds, a distribution demo for a quick terminal sanity check, and a canvas grid to see the spread. The same picker generalizes to boolean and weighted helpers, where the odds are tuned instead of the source array.
Related articles
- How to Create a Random Boolean Function in JavaScript (Node and Browser)
- How to Create a Weighted Random Function in JavaScript (Node and Browser)
- How to Create a JavaScript Module (Node and Browser)