What you'll be able to do

By the end of this page you should be able to pass a function as an argument, call it inside another function, and see why onDone and onDone() are different. Everything here is still synchronous — no timers or network yet — so the idea stays clear.

Previous post: default parameters and rest.

Who this is for

  • People scared of “callback” as a buzzword
  • Beginners who write doWork(myFn()) and wonder why it runs too soon
  • Anyone who already used addEventListener("click", ...) without naming the pattern

You can skip this if you already pass functions around and know not to add () until you mean “run now.” Come back when a handler runs immediately on page load.

A function is a value

You already assign numbers and strings to names. Functions are values too — you can store them, pass them, and return them.

function shout(text) {
  console.log(String(text).toUpperCase());
}

const alsoShout = shout;
alsoShout("hi");

Scenario A — another name for the same function. alsoShout and shout point at the same callable value.

Scenario B — pass it in. Someone else decides when to call it; you decide what it does.

What “callback” means here

A callback is a function you pass so another function can call it later (in this post: later in the same turn of code, not “after a network wait”).

function runTwice(fn) {
  fn();
  fn();
}

function sayHi() {
  console.log("hi");
}

runTwice(sayHi);

Logs hi twice.

Scenario A — reusable runner. runTwice does not know about greetings. It only knows “call whatever you gave me.”

Scenario B — different behavior, same runner.

runTwice(function () {
  console.log("tick");
});

You pass a different function value; runTwice stays the same.

The () trap: pass the function, do not call it yet

function runOnce(fn) {
  fn();
}

function boom() {
  console.log("boom");
}

runOnce(boom); // correct: pass the function
// runOnce(boom()); // wrong: calls boom first, passes undefined

Scenario A — event listeners. button.addEventListener("click", boom) stores the function. The browser calls it on click.

Scenario B — accidental immediate call. addEventListener("click", boom()) runs boom now and registers whatever boom returned (often undefined). The click then does nothing useful.

Rule: when the API wants a callback, pass fn or () => ..., not fn() — unless you intentionally want to pass the result of a call.

Callbacks with data

The outer function can pass arguments into the callback:

function withName(name, fn) {
  fn(name);
}

withName("Asha", function (n) {
  console.log("Hello, " + n);
});

Scenario A — formatters. One function fetches or builds a value; the callback decides how to show it.

Scenario B — button already does this. The browser calls your listener with an event object. You write function (event) { ... }. Same idea: they call you with data.

A tiny practice file

Save as callbacks.html.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Callbacks</title>
  </head>
  <body>
    <button id="go">Run twice</button>
    <pre id="out">Result will show here.</pre>
    <script>
      function runTwice(fn) {
        fn();
        fn();
      }

      document.getElementById("go").addEventListener("click", function () {
        const lines = [];
        runTwice(function () {
          lines.push("tick");
        });
        document.getElementById("out").textContent = lines.join("\n");
      });
    </script>
  </body>
</html>

You should see two tick lines. The click handler is itself a callback; inside it, runTwice takes another callback.

Mistakes I see a lot

1. doWork(myFn()) when you meant doWork(myFn). Runs too early.

2. Forgetting the callback never runs if nobody calls it. Passing is not calling.

3. Expecting async timing here. Sync callbacks still run now, in order — just “later” inside the host function.

4. Naming every parameter callback with no meaning. onSuccess, render, fn — pick a name that says the job.

5. Huge inline callbacks. Extract a named function when the body grows.

What to try before the next post

  1. Write runTwice(fn) and pass two different functions.
  2. Break it once with runTwice(sayHi()) and fix it.
  3. Pass a callback that receives a string argument.
  4. Build the tiny HTML file.

Next in this series: higher-order functions — functions that take or return functions, and why map needs a function argument.

Try this next outside the series

Functions matter more once they stop being textbook examples and start shaping reusable app code.

  • Node.js async patterns — see functions return promises, accept callbacks, and coordinate real work
  • Diff Checker — compare two function versions before you keep a refactor