A random boolean — a coin flip that returns 1 or 0 about half the time — is the smallest building block for simulations and generative art. The function is one line; the useful part is confirming it lands on each value roughly equally and never returns anything else. This tutorial writes the function as an ES module, pins its rounding behavior with a Node test suite, prints a distribution, and paints the flips on a browser canvas.

Prerequisites

  • Node.js 18 or later (validated with Node 26.4.0)
  • Python 3 for the local static server (python3 ships 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. If you have not set up an ES-module project before, see How to Create a JavaScript Module (Node and Browser).

Step 1: Create the project and its package file

The "type": "module" field lets Node treat .js files as ES modules, so import/export work with no build step.

Create the file

mkdir -p js-coinflip-101
cd js-coinflip-101
touch package.json

Add the code: js-coinflip-101/package.json

{
  "name": "js-coinflip-101",
  "version": "1.0.0",
  "description": "A random 1/0 coin-flip function for JavaScript (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, import throws SyntaxError: Cannot use import statement outside a module.
  • The demo and test scripts alias the two commands used later; there are no runtime dependencies.

Step 2: Write the coinFlip function

Create the file

touch coinflip.js

Add the code: js-coinflip-101/coinflip.js

// File: coinflip.js
// Return a random 1 or 0, roughly 50/50.

export const coinFlip = () => Math.round(Math.random());

Detailed breakdown

  • Math.random() returns a float in the range [0, 1) — always at least 0 and always less than 1.
  • Math.round() maps that to an integer: values from 0 up to (but not including) 0.5 round to 0, and values from 0.5 up to (but not including) 1 round to 1. Each half of the range has the same width, so the two outcomes are equally likely.
  • In JavaScript 1 is truthy and 0 is falsy, so the result doubles as a boolean when used in a condition. Step 3’s tests pin down both the boundary at 0.5 and the two ends.

Step 3: Pin the behavior with tests

Node’s built-in test runner (node --test, stable since Node 20) covers the parts that must always hold, with no framework to install.

Create the file

mkdir -p test
touch test/coinflip.test.js

Add the code: js-coinflip-101/test/coinflip.test.js

// File: test/coinflip.test.js
// Runs with the built-in Node test runner: `node --test`.

import { test } from "node:test";
import assert from "node:assert/strict";

import { coinFlip } from "../coinflip.js";

// Run `fn` with Math.random pinned to `value`, then restore it.
function withRandom(value, fn) {
  const original = Math.random;
  Math.random = () => value;
  try {
    return fn();
  } finally {
    Math.random = original;
  }
}

test("always returns 1 or 0", () => {
  for (let i = 0; i < 1000; i++) {
    const result = coinFlip();
    assert.ok(result === 0 || result === 1);
  }
});

test("rounds below 0.5 down to 0", () => {
  assert.equal(withRandom(0, coinFlip), 0);
  assert.equal(withRandom(0.49, coinFlip), 0);
});

test("rounds 0.5 and above up to 1", () => {
  assert.equal(withRandom(0.5, coinFlip), 1);
  assert.equal(withRandom(0.9999, coinFlip), 1);
});

test("both outcomes appear over many flips", () => {
  const seen = new Set(Array.from({ length: 500 }, () => coinFlip()));
  assert.deepEqual([...seen].sort(), [0, 1]);
});

Detailed breakdown

  • withRandom replaces the global Math.random with a stub that returns a fixed value, runs the function, then restores the original in a finally block so a failed assertion cannot leak the stub into a later test.
  • The two rounding tests nail the 0.5 boundary from both sides: 0.49 rounds to 0, 0.5 rounds to 1, and 0.9999 (the top of the range) still rounds to 1, never to 2.
  • The first test is a property check across 1000 flips (capturing the result once so it asserts on a single flip); the last collects 500 flips into a Set and confirms both values showed up.

Run the suite:

node --test
✔ always returns 1 or 0
✔ rounds below 0.5 down to 0
✔ rounds 0.5 and above up to 1
✔ both outcomes appear over many flips
ℹ tests 4
ℹ pass 4
ℹ fail 0

Step 4: Print the distribution

A quick sanity check is to flip many times and count each outcome. With a fair coin the two counts should land near an even split.

Create the file

touch demo.js

Add the code: js-coinflip-101/demo.js

// File: demo.js
// Flip the coin 100 times and count how many 1s and 0s came up.

import { coinFlip } from "./coinflip.js";

const LIMIT = 100;

const flips = Array.from({ length: LIMIT }, () => coinFlip());
console.log(flips);

const occurrences = flips.reduce((acc, value) => {
  acc[value] = (acc[value] || 0) + 1;
  return acc;
}, {});
console.log(occurrences);

Detailed breakdown

  • Array.from({ length: LIMIT }, () => coinFlip()) collects 100 flips into an array.
  • The reduce tallies each value into a counts object; the counts always sum to LIMIT.

Run it:

node demo.js

The output varies on every run. One run produced:

[
  1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1,
  0, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,
  0, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 0,
  0, 0, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0,
  0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 1,
  1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1,
  0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0,
  1, 1, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1,
  0, 0, 0, 0
]
{ '0': 44, '1': 56 }

An exact 50/50 split is rare; most runs land within a 40/60 spread, and a lopsided run now and then is expected with only 100 flips.

Step 5: Visualize the flips in the browser

A grid makes the balance obvious at a glance. This version fills a 10x10 grid with flips and paints each 1 green and each 0 black.

Start with the page. Create index.html:

Create the file

touch index.html

Add the code: js-coinflip-101/index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>js-coinflip-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 width/height attributes set the canvas drawing buffer to 300x300; the CSS sizes how it appears on screen.
  • <script type="module"> loads app.js as an ES module so it can import the coin-flip function.
  • <canvas> takes an explicit closing tag; it is not a void element and cannot self-close.

Add the stylesheet. Create app.css:

Create the file

touch app.css

Add the code: js-coinflip-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: absolute with all four offsets at 0 plus margin: auto centers the canvas both ways.
  • The 400px display size scales up the 300x300 drawing buffer.

Now the drawing code. Create app.js:

Create the file

touch app.js

Add the code: js-coinflip-101/app.js

// File: app.js
// Fill a 10x10 grid with coin flips: green for 1, black for 0.

import { coinFlip } from "./coinflip.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_GREEN = "#39FF14";

// One coin flip per grid cell.
const cells = Array.from({ length: DIM * DIM }, () => coinFlip());
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++] ? NEON_GREEN : "black";
      ctx.fillRect(
        i * CELL_SIZE + BORDER,
        j * CELL_SIZE + BORDER,
        CELL_SIZE - BORDER * 2,
        CELL_SIZE - BORDER * 2,
      );
    }
  }
}

Detailed breakdown

  • DIM * DIM is 100, so cells holds one flip per grid cell.
  • CELL_SIZE is SCREEN_SIZE / DIM (30 pixels); BORDER shrinks each painted square by one pixel per side so the black background reads as gridlines.
  • The ternary cells[cursor++] ? NEON_GREEN : "black" paints a 1 green and leaves a 0 on the black background. With a fair coin, close to half the cells light up.

Step 6: Serve the page and drive it with a Makefile

Browsers block module imports over file://, so serve the folder over HTTP. A short Makefile wraps the demo, tests, and server.

Create the file

touch Makefile

Add the code: js-coinflip-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: ## Flip the coin 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 := help makes a bare make print the help screen.
  • PORT ?= 8000 sets a default port a caller can override, e.g. make serve PORT=9000.

Confirm the help screen, then serve:

make
Targets:
  demo     Flip the coin 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)

Run make serve, open http://localhost:8000/ in a browser, and reload a few times. Each load repaints the grid with a fresh set of flips, and roughly half the cells glow green. Press Ctrl-C to stop the server.

Recap

The function is Math.round(Math.random()). The work around it is what makes it trustworthy: "type": "module" for clean imports, a node --test suite that stubs Math.random to pin the 0.5 boundary and both ends, a distribution demo, and a canvas grid to see the balance. When you need something other than a 50/50 split, reach for a weighted version instead.

References