Inside the draw: randomness, shuffles and retries
By Shangyi Duan · Last verified 2026-11-02 · Sources: src/logic.js draw() and src/worker.js · Report an error
A code-level walkthrough of what happens between pressing Draw and seeing one name: the random source, the shuffle, the search that backtracks instead of retrying, and the two ways it can refuse.
1. The random source
The server does not use Math.random. The worker builds a generator on the platform's cryptographic API and passes it into the draw:
const crng = () => crypto.getRandomValues(new Uint32Array(1))[0] / 4294967296;
That call has 2^32 possible outputs per draw, and every random decision in the algorithm is one call to it. The draw function takes the generator as its third argument (draw(names, exclusions, rng)), which is why the same function can also be driven by the fixed seed used on the demo page.
2. Names and exclusions are cleaned first
cleanNames() trims each name, collapses repeated spaces, cuts it to 40 characters and rejects a case-insensitive duplicate with Duplicate name: …. Then every exclusion pair is added to a blocklist in both directions, so adding Ana and Ben blocks Ana→Ben and Ben→Ana in one step. A pair that names someone who is not in the list, or names the same person twice, is ignored rather than throwing. Before any search starts, one check catches hopeless rules: if a person is excluded from every other participant, the draw throws <name> is excluded from everyone. Remove a rule. and stops there.
3. The shuffle
Two shuffles run, both Fisher–Yates, walking from the last index down and swapping each item with a random earlier one:
for (let i = a.length - 1; i > 0; i--) {
const j = rnd(i + 1, rng);
[a[i], a[j]] = [a[j], a[i]];
}
The first shuffle randomises the order in which people are placed. That list is then sorted by exclusion count, highest first, so the most constrained people choose before the less constrained ones. The sort is stable, so the shuffle decides the order among people with the same count. The second shuffle orders the candidate receivers at every step of the search, which is what stops the result from following the order the names were typed in.
4. The search, and why there is no retry count
The algorithm is a depth-first search that backtracks. At each level it takes the next person in the placement order and tries receivers in shuffled order. A candidate is skipped when it is the giver, already taken, or blocked:
if (r === g || used[r] || bad[g].has(r)) continue;
When a person runs out of candidates, the search returns to the previous person and tries their next option — that is the retry. There is no "try the whole draw N times" loop, so there is no N to quote. The only hard stop is a step counter shared by the entire draw:
if (++steps > 200000) return false;
Every placement attempt counts once. If the budget runs out, or the search exhausts every candidate and no complete assignment exists, draw() throws No valid draw is possible with these rules. Remove an exclusion. The API turns that into HTTP 400 and no group is stored: no ID, no links, no half-finished draw.
5. Why you cannot draw yourself
The check r === g is the first condition in the candidate filter, so a person is never even considered as their own receiver. The result is also a permutation by construction: used[] marks a receiver as taken, and every person must be placed for the search to succeed. There is no separate "remove self-matches" pass afterwards that could fail — the constraint is part of the search.
6. The same code, on the demo data
This is the five-person family example from the demo page, computed by draw() with the seeded generator (seed 20261102) so it can be re-run. Person A and Person B are the excluded couple:
| Person | Draws |
|---|---|
| Person A | Person C |
| Person B | Person D |
| Person C | Person E |
| Person D | Person B |
| Person E | Person A |
Read the result as cycles: Person A→Person C→Person E→Person A; Person B→Person D→Person B. Every person gives once and receives once, and Person A and Person B never meet.
7. What the code does not do
- No seed receipt. The live draw does not publish the values it drew, so nobody can replay it. The check available to a participant is consistency: the same private link always shows the same one person.
- No weighted or uniform sampling with exclusions. The search returns the first complete assignment it reaches. Legal and unpredictable, but not equally likely across all valid matchings.
- No audit log. The organiser reveal is stamped with a time; that is not a record of who looked at what.
The plain-language version with every error message and what the organiser can see is on how the Secret Santa draw works. The method behind the pages is on methodology.
Updated November 2, 2026