DEV Community

Brian Kipchirchir
Brian Kipchirchir

Posted on

ALL YOU NEED TO KNOW ABOUT FUNCTION VARIATIONS

๐Ÿš€ The Complete Guide to JavaScript Functions, return, Closures & Async Code

A beginner-friendly reference that explains not just what happens in each
code variation, but why โ€” so nothing here ever feels like a surprise. ๐ŸŽฏ


๐Ÿ“ PART 1: The Foundational Rules (read this first!)

Almost every "confusing" JavaScript output comes down to just two ideas.
Once these are second nature, every example later in this guide is just an
application of them. ๐Ÿง 

๐Ÿฅ‡ Rule 1: Printing and returning are two completely different actions

  • console.log(...) ๐Ÿ–จ๏ธ is an instruction that says: "display this on the screen, right now." It's called a side effect โ€” something that happens while code is running, as a byproduct of running it.
  • return ๐Ÿ“ฆ is an instruction that says: "stop running this function, and hand this specific value back to whoever called it."

A function can do one, both, or neither. Printing something does not
automatically return it, and returning something does not automatically
print it. They are unrelated unless you write code that connects them.

๐Ÿฅˆ Rule 2: A function call is an expression, and it always evaluates to something

Every time you call a function โ€” fn() โ€” that call itself is a small
expression that resolves to a value, the same way 2 + 2 resolves to 4.

  • If the function has a return value; that executes, the call resolves to value. โœ…
  • If the function finishes without any return executing (or has a bare return; with nothing after it), the call resolves to undefined. ๐Ÿ‘ป This isn't a bug or an edge case โ€” it's JavaScript's built-in default.

Why does this matter so much? Because anything that "asks" for the
result of a function call โ€” wrapping it in console.log(...), assigning it
with let result = ..., using it in a calculation โ€” is asking for the
return value
, not for whatever the function happened to print while it
was running. Those are two separate, independent things happening at two
separate moments.

๐Ÿฅ‰ Rule 3: Code runs top to bottom, and JavaScript won't skip ahead

When you write console.log(fn()), JavaScript can't print anything until
it knows what fn() evaluates to. So it fully runs fn() first โ€” including
any console.log statements inside fn โ€” and only once fn has finished
and produced a return value does the outer console.log get to run.

This is why you'll see "inner" prints appear before "outer" prints in the
console, every time โ€” the inner function has to finish completely before
control returns to whatever called it.

๐Ÿ’ก Keep these three rules in your head as you read every example below.
Each one is just these rules playing out in a slightly different shape.


๐Ÿงฉ PART 2: Function Basics

๐Ÿค” What is a function?

A function is a reusable block of code built to perform a specific task.
Functions let you organize, reuse, and modularize code instead of repeating
the same logic everywhere. โ™ป๏ธ

function greet(name) {
  console.log("Hello " + name);
}
greet("Alice");   // Hello Alice
Enter fullscreen mode Exit fullscreen mode
  • name, inside the parentheses of the definition, is called a parameter โ€” a placeholder variable that doesn't hold a real value until the function is called.
  • "Alice", passed in when calling the function, is called an argument โ€” the actual, real value supplied for that call.
  • greet("Alice"); is calling (or invoking) the function โ€” this is the moment the code inside greet actually runs. โ–ถ๏ธ

๐ŸŽ›๏ธ Default parameters

A parameter can be given a fallback value, used automatically whenever no
argument (or undefined) is passed in for it:

function greet(name = "Guest") {
  console.log("Hello, " + name);
}
greet();          // Hello, Guest   (no argument โ†’ default is used)
greet("Alice");    // Hello, Alice   (argument overrides the default)
Enter fullscreen mode Exit fullscreen mode

๐Ÿ” PART 3: return vs console.log โ€” Every Variation

Every example below uses a = 5, b = 10 (so a + b = 15) to keep the
numbers easy to track โ€” only the structure of the code changes. ๐Ÿงช

Category A: Calling the function plainly (not wrapped in an outer console.log)

A1. No console.log inside, no return

function fn(a, b) { }
fn(5, 10);
Enter fullscreen mode Exit fullscreen mode

Output: (nothing at all) ๐Ÿคท
Why: There's no console.log to produce a side effect, and nothing is
asking for the return value either. The function quietly does nothing
visible.

A2. console.log inside, no return

function fn(a, b) { console.log(a + b); }
fn(5, 10);
Enter fullscreen mode Exit fullscreen mode

Output: 15 โœ…
Why: The inner console.log fires as a side effect the moment the
function runs. There's no return, so the call does evaluate to
undefined behind the scenes โ€” but since nothing is capturing or printing
that return value here, it's simply never shown.

A3. No console.log inside, has return

function fn(a, b) { return a + b; }
fn(5, 10);
Enter fullscreen mode Exit fullscreen mode

Output: (nothing) ๐Ÿ•ณ๏ธ
Why: 15 is computed and successfully returned โ€” but the returned
value has nowhere to go. It's not stored in a variable, not logged,
nothing. A returned value that nobody uses is simply discarded.

A4. console.log inside AND return

function fn(a, b) { console.log(a + b); return a + b; }
fn(5, 10);
Enter fullscreen mode Exit fullscreen mode

Output: 15 โœ…
Why: Only the inner console.log produces visible output. The
returned 15 is still discarded, same as A3, because nothing on the
outside is asking for it.

Category B: Wrapping the call in an outer console.log

This is where the return value finally becomes visible, because
something is now explicitly asking for it. ๐Ÿ‘€

B1. No log inside, no return

function fn(a, b) { }
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output: undefined ๐Ÿ‘ป
Why: Nothing prints inside fn. The call fn(5, 10) evaluates to
undefined (no return), and the outer console.log prints exactly that.

B2. Log inside, no return

function fn(a, b) { console.log(a + b); }
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output:

15
undefined
Enter fullscreen mode Exit fullscreen mode

Why: Two separate console.log calls fire, in order. First, the one
inside fn runs (side effect โ†’ 15). Then fn finishes with no
return, so the call evaluates to undefined โ€” and that's what the
outer console.log prints.

B3. No log inside, has return

function fn(a, b) { return a + b; }
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output: 15 โœ…
Why: Nothing prints inside fn this time. The outer console.log
prints the returned value directly โ€” just one line, because there was
never a second console.log to fire.

B4. Log inside AND return

function fn(a, b) { console.log(a + b); return a + b; }
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output:

15
15
Enter fullscreen mode Exit fullscreen mode

Why: Two 15s, but for entirely different reasons. The first is the
side effect from the inner console.log. The second is the outer
console.log printing the value that return sent back out โ€” it's only a
coincidence that they're the same number, because in this example both the
printed and returned values happened to be a + b.

Category C: Storing in a variable first

function fn(a, b) { console.log(a + b); return a + b; }
let result = fn(5, 10);
console.log(result);
Enter fullscreen mode Exit fullscreen mode

Output:

15
15
Enter fullscreen mode Exit fullscreen mode

Why: This behaves identically to wrapping the call directly in
console.log() (Category B). let result = fn(5, 10) is simply asking for
the return value and storing it for later โ€” it's the same underlying
operation as B4, just split across two lines instead of one.

Category D: Calling the function twice โ€” once bare, once wrapped

function fn(a, b) { console.log(a + b); }
fn(5, 10);                 // bare call
console.log(fn(5, 10));    // wrapped call
Enter fullscreen mode Exit fullscreen mode

Output:

15
15
undefined
Enter fullscreen mode Exit fullscreen mode

Why: Each call to fn is a completely independent execution โ€” nothing
is remembered or shared between them. The first call runs, its inner log
fires (15), and its (unused) return value is discarded. The second call
runs again from scratch, its inner log fires again (15), and this time
its return value (undefined, since there's no return) is printed by the
outer console.log.

Category E: return positioning and reachability

E1. Code written after return never runs โ›”

function fn(a, b) {
  console.log(a + b);
  return a + b;
  console.log("never runs");   // dead code
}
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output:

15
15
Enter fullscreen mode Exit fullscreen mode

Why: return doesn't just supply a value โ€” it also immediately exits
the function the instant it executes. Any code physically written after a
return statement, in the same execution path, can never run. JavaScript
(and most languages) call this "dead code" โ€” some code editors will even
grey it out or warn you about it.

E2. Bare return; with no value

function fn(a, b) {
  console.log(a + b);
  return;
}
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output:

15
undefined
Enter fullscreen mode Exit fullscreen mode

Why: return; with nothing after it still exits the function
immediately โ€” but since no value was given to return, it behaves exactly
like having no return at all. The call evaluates to undefined.

E3. return inside a conditional that ISN'T met ๐Ÿšง

function fn(a, b) {
  console.log(a + b);
  if (a > 100) {
    return a + b;
  }
  // nothing else here โ€” falls through to the end
}
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output:

15
undefined
Enter fullscreen mode Exit fullscreen mode

Why: Since a is 5, the condition a > 100 is false, so the code
inside the if block โ€” including its return โ€” never runs. Execution
just reaches the natural end of the function with no return having fired,
so it implicitly returns undefined. This is one of the sneakiest sources
of unexpected undefineds: the return is there in the code, but it
simply never got triggered for this particular input.

E4. Same shape, but the condition IS met

function fn(a, b) {
  console.log(a + b);
  if (a > 0) {
    return a + b;
  }
}
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output:

15
15
Enter fullscreen mode Exit fullscreen mode

Why: This time a > 0 is true (since a is 5), so the return
inside the if block does execute, sending 15 back out as expected.

Category F: Returning the result of a console.log() call

function fn(a, b) {
  return console.log(a + b);
}
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output:

15
undefined
Enter fullscreen mode Exit fullscreen mode

Why: return console.log(a + b); looks like it should "return the
number," but it actually returns whatever console.log itself returns
โ€” and console.log's own job is only to print things to the screen; as a
function, it always evaluates to undefined, no matter what you pass it.
So the inner console.log(a + b) prints 15 as a side effect, but its
return value (which is what gets sent back out of fn) is undefined โ€”
and that's what the outer console.log ends up printing.

Category G: Multiple console.logs inside one function

function fn(a, b) {
  console.log("Step 1: adding");
  console.log(a + b);
  return a + b;
}
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output:

Step 1: adding
15
15
Enter fullscreen mode Exit fullscreen mode

Why: Every console.log inside a function fires in the exact order
it's written, as the function executes line by line. None of these inner
logs affect what eventually gets returned โ€” they're just separate,
independent side effects happening along the way. Only the final return
value is what the outer console.log receives.


๐Ÿน PART 4: Arrow Functions โ€” the Hidden Auto-Return Rule

Arrow functions have a special shortcut behavior that regular functions
don't have โ€” and it depends entirely on whether you use curly braces. ๐ŸŽฉ

โœจ Concise body (no braces) โ€” automatically returns the expression

const add = (a, b) => a + b;
console.log(add(2, 3));   // 5
Enter fullscreen mode Exit fullscreen mode

Here, whatever comes right after => is the function's result,
automatically โ€” no return keyword needed. JavaScript treats the entire
arrow function body as a single expression to evaluate and hand back.

๐Ÿ“ฆ Block body (curly braces {}) โ€” behaves exactly like a normal function

const add = (a, b) => {
  a + b;   // computed... then thrown away. NOT returned!
};
console.log(add(2, 3));   // undefined
Enter fullscreen mode Exit fullscreen mode

Why does adding {} change everything? The moment JavaScript sees the
opening {, it stops treating the body as "one expression to auto-return"
and starts treating it as a block of statements โ€” potentially many
lines, with loops, conditionals, multiple calculations, whatever you want.
Once you're in "block mode," JavaScript can no longer guess which line
you meant to return, so it requires you to say so explicitly with return,
exactly like a regular function declaration would.

Fix: ๐Ÿ”ง

const add = (a, b) => {
  return a + b;
};
console.log(add(2, 3));   // 5
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“Š Quick-reference table

Arrow function style Auto-returns?
(a, b) => a + b โœ… Yes โ€” no braces, single expression
(a, b) => { a + b } โŒ No โ€” braces mean you need return
(a, b) => { return a + b } โœ… Yes โ€” because of the explicit return

๐Ÿ•ณ๏ธ The object-literal trap

This same brace rule creates a subtle problem if you're trying to
auto-return an object:

// WRONG: โŒ
const makeUser = (name) => { name: name };
console.log(makeUser("Alice"));   // undefined (or a syntax quirk)
Enter fullscreen mode Exit fullscreen mode

Here, JavaScript sees the { right after => and assumes it's the start
of a function block โ€” not the start of an object literal. So
{ name: name } gets misread as a labeled statement rather than a { key:
value }
object, and nothing gets properly returned.

Fix โ€” wrap the object in parentheses: โœ…

const makeUser = (name) => ({ name: name });
console.log(makeUser("Alice"));   // { name: "Alice" }
Enter fullscreen mode Exit fullscreen mode

The parentheses ( ) signal to JavaScript "this is an expression (an
object), not a block of code" โ€” which restores the auto-return behavior
and lets the object be returned correctly.


โณ PART 5: return on the Same Line vs. the Next Line (the ASI Trap)

This is one of the most surprising JavaScript quirks, caused by a feature
called Automatic Semicolon Insertion (ASI). ๐Ÿฅท

๐Ÿคจ Why this happens

JavaScript doesn't strictly require semicolons at the end of every line โ€”
it tries to automatically insert them where it thinks a statement ends.
For the return keyword specifically, there's a rule: if a line break
immediately follows return, JavaScript automatically inserts a semicolon
right there
, ending the statement on the spot โ€” even if you intended the
next line to be part of the same return.

โŒ The broken version

function getValue() {
  return
  {
    value: 42
  };
}
console.log(getValue());
Enter fullscreen mode Exit fullscreen mode

Output: undefined ๐Ÿ‘ป

Why: JavaScript reads this as if you'd written:

function getValue() {
  return;              // โ† semicolon auto-inserted here!
  {
    value: 42
  };
}
Enter fullscreen mode Exit fullscreen mode

The return on its own line is immediately treated as a complete, bare
return; statement โ€” which, as covered in Category E2, returns
undefined. Everything after it (the object) becomes unreachable dead
code, exactly like Category E1 โ€” it's never even evaluated, let alone
returned.

โœ… The fix โ€” keep return and its value on the same line

function getValue() {
  return {
    value: 42
  };
}
console.log(getValue());   // { value: 42 }
Enter fullscreen mode Exit fullscreen mode

As long as the { (or whatever value you're returning) appears on the
same line as the return keyword, JavaScript knows the statement isn't
finished yet, and doesn't insert a semicolon early. It's only a line break
directly after return, with nothing else on that line, that triggers
this trap.

๐ŸŽฏ The takeaway rule: always keep return and the value you're
returning on the same line. If a returned value is long and you want to
wrap it across multiple lines for readability, wrap starting from an
opening bracket/brace/parenthesis on the same line as return, like
the fixed example above โ€” never leave return dangling alone at the end
of a line.


๐Ÿ•ธ๏ธ PART 6: Nested Function Calls โ€” Tracing Values Through Multiple Functions

This is one of the more complex patterns, because control has to jump
between multiple functions before a final value comes out. Let's build it
up slowly. ๐Ÿง—

โœ… The working version

function double(n) { return n * 2; }
function fn(a, b) { return double(a + b); }
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output: 30 ๐ŸŽ‰

Step-by-step trace:

  1. double and fn are just defined first โ€” nothing runs yet.
  2. console.log(fn(5, 10)) is reached. JavaScript must fully evaluate fn(5, 10) before console.log can do anything.
  3. fn is called with a = 5, b = 10. Its only line is return double(a + b); โ€” but to return something, JavaScript must first figure out what double(a + b) actually evaluates to.
  4. a + b is computed first: 5 + 10 = 15.
  5. double(15) is now called. Execution jumps into the double function โ€” fn is paused, waiting, while double runs. โธ๏ธ
  6. Inside double, n = 15. The line return n * 2; runs: 15 * 2 = 30. double returns 30 and finishes.
  7. Execution jumps back to exactly where it left off inside fn โ€” the double(a + b) call has now fully resolved to 30. โ–ถ๏ธ
  8. fn's statement effectively becomes return 30;. fn returns 30 and finishes.
  9. Execution jumps back once more, to the original console.log(fn(5, 10)) line โ€” which now has the value it needed: 30. It prints it.

Visualizing the call stack (who's "inside" who, and in what order): ๐Ÿ—บ๏ธ

console.log( fn(5, 10) )
     โ†“ must evaluate fn(5, 10) first
  fn runs, a=5, b=10
     โ†“ must evaluate double(a + b) first
  double(15) is called
     โ†“
  double runs, n=15
     โ†“
  return n * 2  โ†’  resolves to 30
     โ†“ double is done โ€” hands 30 back to fn
  fn's return becomes: return 30
     โ†“ fn is done โ€” hands 30 back to console.log
  console.log(30)
     โ†“
  prints: 30
Enter fullscreen mode Exit fullscreen mode

The key idea: ๐Ÿ’ก return values chain cleanly through nested calls, as
long as every function in the chain properly uses return. fn doesn't
need to know or care how double calculates its answer โ€” it just calls
double(...), waits for it to finish, and immediately forwards whatever
comes back.

โš ๏ธ The broken version โ€” a missing return breaks the whole chain

function double(n) { console.log(n * 2); }   // no return!
function fn(a, b) { return double(a + b); }
console.log(fn(5, 10));
Enter fullscreen mode Exit fullscreen mode

Output:

30
undefined
Enter fullscreen mode Exit fullscreen mode

What changed: double now only prints 30 โ€” it no longer returns
anything. So when fn calls double(a + b) and tries to return whatever
comes back, what actually comes back is undefined (since double has no
return of its own).

This is the critical lesson of nested calls: even though fn does
have its own return statement, fn is only as good as the value it's
returning. return double(a + b) doesn't guarantee a "real" value โ€” it
only guarantees that fn forwards along whatever double(a + b)
evaluates to, good or bad. A missing return anywhere in the chain โ€” even
several layers deep โ€” poisons every function built on top of it, because
undefined gets faithfully passed up the entire chain, layer by layer,
exactly as written. ๐ŸงŸ

๐ŸŽฏ The takeaway: when chaining function calls together, check every
function in the chain, not just the outermost one. A return at the top
level means nothing if it's just forwarding an undefined that came from
somewhere deeper inside.


๐Ÿ”— PART 7: Closures

A closure happens when an inner function "remembers" and can still
access variables from the outer function it was created inside โ€” even
after that outer function has already finished running and would normally
have disappeared. ๐Ÿง โœจ

function counter() {
  let value = 0;
  return function () {
    value++;
    return value;
  };
}
Enter fullscreen mode Exit fullscreen mode

Normally, value would only exist while counter() is actively running,
and would vanish once counter() finishes. But because the inner function
is returned and kept around, it keeps a permanent link back to that exact
value variable โ€” this link is the closure.

โš ๏ธ Common mistake โ€” calling counter() again resets everything

console.log(counter()());   // 1
console.log(counter()());   // 1 again! โ€” NOT 2
Enter fullscreen mode Exit fullscreen mode

Why: each counter() call creates a brand-new, completely separate
value variable, starting fresh at 0. Calling counter() twice doesn't
reuse anything from the first call โ€” it's like renting two completely
different apartments; nothing in one affects the other. ๐Ÿ ๐Ÿ 

โœ… Correct โ€” store the returned function once, and reuse that

let getValue = counter();   // creates ONE closure โ€” one value, starting at 0
console.log(getValue());     // 1
console.log(getValue());     // 2 โ€” same closure, state carries over
console.log(getValue());     // 3
Enter fullscreen mode Exit fullscreen mode

The key idea: ๐Ÿ’ก getValue isn't just "a function" โ€” it's the
specific
inner function tied to one particular value variable, created
the one time counter() was called. Every time you call getValue(),
you're reaching into that same preserved variable and updating it โ€” which
is exactly what makes closures useful for things like counters, private
state, and memoized calculations.


โฐ PART 8: setTimeout โ€” What It Is and Why You'd Use It

๐Ÿค” What is setTimeout?

setTimeout is a built-in JavaScript function that means: "run this code
later, after waiting at least this many milliseconds."

setTimeout(() => {
  console.log("This runs later");
}, 1000);

console.log("This runs first");
Enter fullscreen mode Exit fullscreen mode

Output:

This runs first
This runs later
Enter fullscreen mode Exit fullscreen mode

Even though "This runs later" is written above "This runs first" in
the code, it prints second. setTimeout doesn't pause your program to
wait โ€” it just schedules the function to run after the delay, and lets the
rest of your code keep going immediately. ๐Ÿƒโ€โ™‚๏ธ๐Ÿ’จ

๐ŸŽ›๏ธ Its two arguments

setTimeout(callbackFunction, delayInMilliseconds);
Enter fullscreen mode Exit fullscreen mode
  1. A callback function โ€” the code you want to run later.
  2. A delay in milliseconds โ€” the minimum wait before running it. 1000 = 1 second. Even 0 still waits for the current code to finish first (see below).

๐Ÿงต Why does JavaScript behave this way?

JavaScript runs on a single thread โ€” it can only do one thing at a
time. If setTimeout literally froze everything for the delay, your whole
page or program would lock up completely โ€” no clicks, no other code, no
animations โ€” for that entire time. ๐Ÿฅถ

Instead, JavaScript hands the timer off to the browser (or Node.js) to
track in the background, while your main code keeps running without
interruption. Only once the timer finishes and all currently-running
code has completely finished does your scheduled function actually get its
turn to execute. This underlying mechanism is called the event loop. ๐Ÿ”

console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");
Enter fullscreen mode Exit fullscreen mode

Output:

1
3
2
Enter fullscreen mode Exit fullscreen mode

Even with a delay of 0 milliseconds, "2" still prints last โ€” because
setTimeout callbacks always wait for all synchronous (immediate) code to
finish first, no matter how short the delay is.

๐ŸŽฏ Why would you actually need it?

  • Simulating real delays โ€” waiting for a server response, a network request, or "typing" animations. ๐ŸŒ
  • Debouncing/throttling โ€” waiting a moment before reacting to fast, repeated events (like a user typing in a search box), so you don't re-run expensive work on every single keystroke. โŒจ๏ธ
  • Letting the UI breathe โ€” giving the browser time to render or update before running something heavy. ๐ŸŽจ
  • Scheduling reminders or animations โ€” "fade this out after 2 seconds." โฒ๏ธ
  • Retrying failed operations โ€” "if this failed, try again in 5 seconds." ๐Ÿ”„

โŒ Why return doesn't work inside setTimeout

function delayedAdd(a, b) {
  setTimeout(() => {
    return a + b;
  }, 1000);
}
console.log(delayedAdd(5, 10));   // undefined, printed IMMEDIATELY
Enter fullscreen mode Exit fullscreen mode

Full trace:

  1. delayedAdd(5, 10) is called.
  2. setTimeout(...) runs, scheduling the arrow function for 1 second later. This doesn't pause anything โ€” setTimeout itself finishes instantly and execution moves on right away.
  3. delayedAdd has nothing else to do โ€” it reaches the end of its body with no return statement at its own level. So it implicitly returns undefined, immediately, without waiting for the timer at all.
  4. console.log(delayedAdd(5, 10)) prints that undefined right away.
  5. A full second later, the scheduled arrow function finally runs. return a + b; genuinely computes 15 and genuinely returns it โ€” but this return only exits that inner arrow function, handing the value to setTimeout's internal machinery. setTimeout has no built-in way to forward a callback's return value to anyone else โ€” so that 15 is simply computed and then discarded, silently, with no error. ๐Ÿ’จ

โš ๏ธ The core problem in one sentence: delayedAdd finishes running
(and already returned undefined) a full second before the timer even
starts counting down โ€” so there's no way for a return inside the
delayed callback to reach back and change what already happened.


๐Ÿ“ž PART 9: The Callback Pattern โ€” Delivering a Value Later

Since a function can't return a value that doesn't exist yet, the
callback pattern solves this by passing in a function that gets
called once the value is finally ready โ€” instead of relying on return.

function delayedAdd(a, b, callback) {
  setTimeout(() => {
    callback(a + b);
  }, 1000);
}

delayedAdd(5, 10, (result) => {
  console.log(result);   // 15, printed after 1 second
});
Enter fullscreen mode Exit fullscreen mode

๐Ÿ”ฌ Breaking down exactly what's happening

There are two separate functions at play here, doing two separate jobs:

Function 1 โ€” the anonymous function passed to setTimeout: ๐Ÿ“จ

() => { callback(a + b); }
Enter fullscreen mode Exit fullscreen mode
  • Where it lives: passed directly as setTimeout's first argument.
  • Its job: wait to be triggered after 1 second, then compute a + b and hand that number off by calling callback(...).
  • Think of this as the messenger โ€” its only responsibility is to calculate the answer and deliver it onward, once the timer's done.

Function 2 โ€” the function you passed into delayedAdd: ๐Ÿ“ฅ

(result) => { console.log(result); }
Enter fullscreen mode Exit fullscreen mode
  • Where it lives: passed into delayedAdd as its third argument, and captured there under the parameter name callback.
  • Its job: receive whatever value it's handed (as result), and decide what to actually do with it โ€” here, logging it.
  • Think of this as the receiver/handler โ€” it has no idea about timers or a/b at all; it just knows "when someone calls me with a value, act on that value."

๐Ÿงฒ How callback is reached, not "passed," inside setTimeout

A subtle but important detail: the anonymous function inside setTimeout
doesn't receive callback as an argument โ€” it simply reaches out to
callback, a variable that already exists in the surrounding scope (it's
delayedAdd's own parameter). This works because of closure (see Part
7): an inner function automatically has access to variables from the
function that contains it, without needing them explicitly passed in.

๐Ÿ”„ The full chain, step by step

delayedAdd(5, 10, YOUR_FUNCTION) is called
                     โ†“ YOUR_FUNCTION is stored as the parameter "callback"
setTimeout schedules an anonymous function for 1 second later
                     โ†“
        --- 1 second passes --- โณ
                     โ†“
The anonymous function runs (triggered by setTimeout)
                     โ†“ computes a + b โ†’ 15
calls callback(15)
                     โ†“ "callback" IS your original function
Your function runs, with result = 15
                     โ†“
console.log(15) ๐ŸŽ‰
Enter fullscreen mode Exit fullscreen mode

โœ… Why this fixes the original problem

Unlike the broken return a + b; version, callback(a + b) isn't a dead
end โ€” it's an active delivery. Instead of trying (and failing) to hand a
value back through a return that already finished executing, the
callback pattern calls a function directly, at the exact moment the
value becomes available โ€” like making a phone call the instant news
arrives, instead of leaving a letter in a mailbox nobody's checking. ๐Ÿ“ฌโžก๏ธ๐Ÿ“ž


๐Ÿค PART 10: Promises โ€” a More Structured Way to Handle "Later"

๐ŸŽŸ๏ธ What is a Promise?

A Promise is a built-in JavaScript object representing "a value that
doesn't exist yet, but will exist later."
Think of it like an IOU: you
receive the Promise object immediately, but the real value inside it
arrives at some future point.

A Promise is always in one of three states:

  • โณ Pending โ€” still waiting; no result yet.
  • โœ… Fulfilled (resolved) โ€” finished successfully; has a value.
  • โŒ Rejected โ€” something went wrong; has an error instead.

๐Ÿ—๏ธ Creating a Promise

new Promise((resolve) => {
  // ... do something that takes time ...
  resolve(someValue);   // once ready, call resolve() with the result
});
Enter fullscreen mode Exit fullscreen mode

new Promise(...) takes a function โ€” called the executor โ€” that runs
immediately, the instant the Promise is created. The executor
automatically receives a special function, resolve, as its parameter.
Calling resolve(value) is how you tell the Promise "the value is ready โ€”
here it is; switch from pending to fulfilled." ๐Ÿ”“

๐Ÿ”ฌ Full walkthrough

function delayedAdd(a, b) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(a + b);
    }, 1000);
  });
}

delayedAdd(5, 10).then((result) => {
  console.log(result);
});
Enter fullscreen mode Exit fullscreen mode

Output: 15, printed after a 1-second delay. ๐ŸŽ‰

Step by step:

  1. delayedAdd(5, 10) is called.
  2. new Promise((resolve) => {...}) is created. Its executor function runs immediately: it receives resolve as a tool (not a value โ€” just a function reference it can call later), and calls setTimeout(...) to schedule work for 1 second from now.
  3. Important: the executor does not have the result at this point. It hasn't computed anything. Its only job was to start the timer and hold onto the resolve tool for whenever it's needed. The executor finishes running almost instantly.
  4. new Promise(...) (still pending) is what delayedAdd returns โ€” immediately. This is the fix over the broken version: delayedAdd isn't trying to return the sum directly (which doesn't exist yet) โ€” it's returning a Promise object, a placeholder that represents "a sum is coming." ๐Ÿ“ฆโณ
  5. .then((result) => {...}) is attached to that Promise. This registers what should happen later, once the Promise resolves โ€” it does not run yet.
  6. Execution moves on. Nothing is blocked or paused.
  7. One second later, the setTimeout callback finally fires (this is a third, separate function โ€” nested inside the executor, but not the executor itself).
  8. Inside it, a + b is computed โ†’ 15. resolve(15) is called.
  9. Calling resolve(15) updates the Promise object itself โ€” not the executor, which already finished running a second ago โ€” flipping its state to "fulfilled" and storing 15 as its value. โœ…
  10. Because the Promise is now fulfilled, JavaScript automatically triggers the function registered via .then(...), passing it 15 as its result argument.
  11. console.log(result) runs, printing 15. ๐ŸŽŠ

๐ŸŽญ Three separate functions, three separate jobs

Function Runs when Its job
Executor: (resolve) => {...} Immediately, once Starts the timer; receives (but doesn't yet use) the resolve tool
setTimeout callback: () => resolve(a+b) 1 second later Computes the real value and calls resolve() with it
.then() callback: (result) => console.log(result) Right after the Promise resolves Receives the final value and decides what to do with it

These are genuinely three different functions โ€” it's easy to mistakenly
think the executor and the .then() callback are "the same thing," but
they're written in completely different places and run at completely
different times.

โœ… Why this avoids the original problem

Compare directly:

// BROKEN โ€” return goes into a dead end โŒ
function delayedAdd(a, b) {
  setTimeout(() => {
    return a + b;   // setTimeout ignores whatever its callback returns
  }, 1000);
  // implicitly returns undefined immediately
}
Enter fullscreen mode Exit fullscreen mode
// FIXED โ€” resolve() updates a Promise that .then() is actively watching โœ…
function delayedAdd(a, b) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(a + b);   // this goes somewhere โ€” it fulfills the Promise
    }, 1000);
  });
}
Enter fullscreen mode Exit fullscreen mode

The critical difference: resolve isn't a dead end like the stray
return was. resolve is directly tied to the specific Promise object
that delayedAdd already returned โ€” and calling it updates that exact
object, which .then() is actively watching for changes.

โšก The async/await equivalent

async function run() {
  const result = await delayedAdd(5, 10);
  console.log(result);
}
run();
Enter fullscreen mode Exit fullscreen mode

await pauses execution inside the async function until the Promise
resolves โ€” it's essentially .then() rewritten to look synchronous, even
though the same non-blocking timer mechanism is running underneath. Both
versions do exactly the same thing; async/await is just a more readable
way to write the same Promise-based logic. โœจ


๐Ÿ† PART 11: Master Rules โ€” the Short Version

If you forget every detailed example above, remember these: ๐Ÿ“Œ

  1. console.log prints immediately, as a side effect, the instant it runs โ€” regardless of what the function eventually returns (or doesn't).
  2. A function call only ever "hands back" its return value to whatever called it. No return (or a bare return;) always means undefined โ€” no matter what got printed inside along the way.
  3. return immediately exits a function. Any code physically written after it, in the same execution path, never runs.
  4. A line break right after return, with nothing else on that line, silently becomes a bare return; due to automatic semicolon insertion โ€” always keep return and its value on the same line.
  5. Arrow functions only auto-return without curly braces. Add {} and you're back to needing an explicit return, just like a normal function.
  6. Nested function calls forward whatever their inner call actually returned โ€” a missing return anywhere in the chain poisons everything built on top of it.
  7. Closures let an inner function remember outer variables โ€” but only if you reuse the same returned function. Calling the outer function again creates a brand-new, independent closure.
  8. setTimeout never pauses your code. It schedules work for later and immediately moves on โ€” so a return inside its callback can never reach back out to the function that scheduled it.
  9. Callbacks and Promises solve the "value isn't ready yet" problem by delivering the result once it exists โ€” via calling a function (callback) or resolving a Promise (.then()/await) โ€” instead of trying to return something before it's ready.

๐Ÿ“‹ Quick-Reference Summary Tables

Setup Bare call Wrapped in outer console.log
No log inside, no return (nothing) undefined
Log inside, no return prints value prints value, then undefined
No log inside, has return (nothing) prints returned value
Log inside, has return prints value prints value, then prints it again
Mistake ๐Ÿ› What goes wrong Fix ๐Ÿ”ง
No return in a function Call evaluates to undefined, even if it logged something real Add return value;
Arrow function with {} but no return Braces disable auto-return Add explicit return, or drop the braces
Arrow function returning an object literal { } read as a function block, not an object Wrap in parentheses: () => ({ ... })
return alone on its own line ASI inserts a semicolon right after it, making it a bare return; Keep return and its value on the same line
return inside a setTimeout/async callback Only exits that inner callback โ€” never reaches the outer function Use a callback parameter or a Promise instead
Calling the outer closure function again instead of reusing the stored one Resets internal state to its starting value Store the returned function once and reuse that variable
Confusing "printed" with "returned" Assuming an outer console.log(fn()) shows what was logged inside It only ever shows the return value, never internal logs

๐ŸŽ“ You made it to the end! Keep this file handy โ€” every "weird"
JavaScript output you run into will trace back to one of the rules above.
Happy coding! ๐Ÿš€โœจ

Top comments (0)