๐ 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 tovalue. โ - If the function finishes without any
returnexecuting (or has a barereturn;with nothing after it), the call resolves toundefined. ๐ป 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
-
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 insidegreetactually 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)
๐ 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);
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);
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);
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);
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));
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));
Output:
15
undefined
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));
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));
Output:
15
15
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);
Output:
15
15
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
Output:
15
15
undefined
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));
Output:
15
15
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));
Output:
15
undefined
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));
Output:
15
undefined
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));
Output:
15
15
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));
Output:
15
undefined
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));
Output:
Step 1: adding
15
15
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
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
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
๐ 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)
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: object, and nothing gets properly returned.
value }
Fix โ wrap the object in parentheses: โ
const makeUser = (name) => ({ name: name });
console.log(makeUser("Alice")); // { name: "Alice" }
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());
Output: undefined ๐ป
Why: JavaScript reads this as if you'd written:
function getValue() {
return; // โ semicolon auto-inserted here!
{
value: 42
};
}
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 }
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
returnand 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 asreturn, like
the fixed example above โ never leavereturndangling 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));
Output: 30 ๐
Step-by-step trace:
-
doubleandfnare just defined first โ nothing runs yet. -
console.log(fn(5, 10))is reached. JavaScript must fully evaluatefn(5, 10)beforeconsole.logcan do anything. -
fnis called witha = 5, b = 10. Its only line isreturn double(a + b);โ but toreturnsomething, JavaScript must first figure out whatdouble(a + b)actually evaluates to. -
a + bis computed first:5 + 10 = 15. -
double(15)is now called. Execution jumps into thedoublefunction โfnis paused, waiting, whiledoubleruns. โธ๏ธ - Inside
double,n = 15. The linereturn n * 2;runs:15 * 2 = 30.doublereturns30and finishes. - Execution jumps back to exactly where it left off inside
fnโ thedouble(a + b)call has now fully resolved to30. โถ๏ธ -
fn's statement effectively becomesreturn 30;.fnreturns30and finishes. - 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
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));
Output:
30
undefined
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. Areturnat the top
level means nothing if it's just forwarding anundefinedthat 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;
};
}
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
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
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");
Output:
This runs first
This runs later
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);
- A callback function โ the code you want to run later.
-
A delay in milliseconds โ the minimum wait before running it.
1000= 1 second. Even0still 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");
Output:
1
3
2
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
Full trace:
-
delayedAdd(5, 10)is called. -
setTimeout(...)runs, scheduling the arrow function for 1 second later. This doesn't pause anything โsetTimeoutitself finishes instantly and execution moves on right away. -
delayedAddhas nothing else to do โ it reaches the end of its body with noreturnstatement at its own level. So it implicitly returnsundefined, immediately, without waiting for the timer at all. -
console.log(delayedAdd(5, 10))prints thatundefinedright away. - A full second later, the scheduled arrow function finally runs.
return a + b;genuinely computes15and genuinelyreturns it โ but thisreturnonly exits that inner arrow function, handing the value tosetTimeout's internal machinery.setTimeouthas no built-in way to forward a callback's return value to anyone else โ so that15is simply computed and then discarded, silently, with no error. ๐จ
โ ๏ธ The core problem in one sentence:
delayedAddfinishes running
(and already returnedundefined) a full second before the timer even
starts counting down โ so there's no way for areturninside 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
});
๐ฌ 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); }
-
Where it lives: passed directly as
setTimeout's first argument. -
Its job: wait to be triggered after 1 second, then compute
a + band hand that number off by callingcallback(...). - 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); }
-
Where it lives: passed into
delayedAddas its third argument, and captured there under the parameter namecallback. -
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/bat 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) ๐
โ 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
});
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);
});
Output: 15, printed after a 1-second delay. ๐
Step by step:
-
delayedAdd(5, 10)is called. -
new Promise((resolve) => {...})is created. Its executor function runs immediately: it receivesresolveas a tool (not a value โ just a function reference it can call later), and callssetTimeout(...)to schedule work for 1 second from now. -
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
resolvetool for whenever it's needed. The executor finishes running almost instantly. -
new Promise(...)(still pending) is whatdelayedAddreturns โ immediately. This is the fix over the broken version:delayedAddisn'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." ๐ฆโณ -
.then((result) => {...})is attached to that Promise. This registers what should happen later, once the Promise resolves โ it does not run yet. - Execution moves on. Nothing is blocked or paused.
-
One second later, the
setTimeoutcallback finally fires (this is a third, separate function โ nested inside the executor, but not the executor itself). - Inside it,
a + bis computed โ15.resolve(15)is called. - Calling
resolve(15)updates the Promise object itself โ not the executor, which already finished running a second ago โ flipping its state to "fulfilled" and storing15as its value. โ - Because the Promise is now fulfilled, JavaScript automatically
triggers the function registered via
.then(...), passing it15as itsresultargument. -
console.log(result)runs, printing15. ๐
๐ญ 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
}
// 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);
});
}
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();
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: ๐
-
console.logprints immediately, as a side effect, the instant it runs โ regardless of what the function eventually returns (or doesn't). -
A function call only ever "hands back" its
returnvalue to whatever called it. Noreturn(or a barereturn;) always meansundefinedโ no matter what got printed inside along the way. -
returnimmediately exits a function. Any code physically written after it, in the same execution path, never runs. -
A line break right after
return, with nothing else on that line, silently becomes a barereturn;due to automatic semicolon insertion โ always keepreturnand its value on the same line. -
Arrow functions only auto-return without curly braces. Add
{}and you're back to needing an explicitreturn, just like a normal function. -
Nested function calls forward whatever their inner call actually
returned โ a missing
returnanywhere in the chain poisons everything built on top of it. - 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.
-
setTimeoutnever pauses your code. It schedules work for later and immediately moves on โ so areturninside its callback can never reach back out to the function that scheduled it. -
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 toreturnsomething 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)