“It Works on Refresh”

Race Conditions and Timing Bugs in the Browser

You open a page and something is broken. A button does nothing. A canvas is blank. An error appears in the console.

You press refresh.

Everything works.

This is not randomness. This is not the browser being flaky. This is timing.

The Browser Is Doing More Than You Think

When a page loads, the browser is juggling multiple tasks at once:

These steps overlap. They don’t always happen in the neat, top-to-bottom order we imagine when reading code.

The Classic Timing Bug


const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
  

Sometimes this works. Sometimes it fails with:


Uncaught TypeError: Cannot read properties of null
  

The problem is not the API. The problem is that canvas does not exist yet.

null does not always mean “wrong”. Very often, it means “too early”.

Why Refreshing Changes the Outcome

When you refresh the page, several things change:

If your bug depends on which thing finishes first, a refresh can make it disappear — without actually fixing anything.

Race Conditions (Without the Scary Definition)

A race condition happens when the correctness of your code depends on timing.

In the browser, the race is often between:

If your code arrives too early, it loses the race.

Seeing the Problem Visually

This short video does a great job of visualizing how JavaScript execution, the DOM, and timing interact in the browser:

Watching this once makes timing bugs feel far less mysterious.

The Correct Way to Synchronize

Instead of hoping your code runs at the right time, tell the browser explicitly when it is allowed to run:


document.addEventListener("DOMContentLoaded", () => {
  const canvas = document.getElementById("canvas");
  const ctx = canvas.getContext("2d");
});
  

This is not a workaround. This is synchronization.

Takeaways