🔴 ISSUE: Returning data before async work finishes
function getUsers() {
let users = [];
setTimeout(() => {
users = [
{ username: 'john', email: 'john@test.com' },
{ username: 'jane', email: 'jane@test.com' },
];
}, 1000);
return users;
}
function findUser(username) {
const users = getUsers();
const user = users.find((user) => user.username === username);
console.log(user);
}
findUser('john')findUser('john')callsgetUsers().getUsers()creates an empty array and starts asetTimeout.JavaScript does NOT wait for the timeout.
getUsers()immediately returns the empty array[].Back in
findUser,usersis[], so.find()returns undefined.The
setTimeoutupdates theusersafter 1 second, but by then the function already finished.
undefined✅ SOLUTION: Use Callback
function getUsers(callback) {
setTimeout(() => {
callback([
{ username: 'john', email: 'john@test.com' },
{ username: 'jane', email: 'jane@test.com' },
]);
}, 1000);
}
function findUser(username, callback) {
getUsers((users) => {
const user = users.find((user) => user.username === username);
callback(user);
});
}
findUser('john', console.log);findUser('john', console.log)is called.Inside
findUser, it callsgetUsers.getUsersusessetTimeoutand after 1 second it runs the callback and sends the users list.Once users arrive,
findUsersearches for"john"in the list.The found user is passed to the callback → which is
console.log.So the user object gets printed.
{ username: 'john', email: 'john@test.com' }<div class="infographic-asyncbug">
<style>
.infographic-asyncbug {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
--bad: #c8452e;
--bad-soft: #f7e3df;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-asyncbug[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
--bad: #e0715a;
--bad-soft: #2e211d;
}
.infographic-asyncbug * { box-sizing: border-box; }
.infographic-asyncbug h1, .infographic-asyncbug h2 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-asyncbug p { margin: 0; }
.infographic-asyncbug button { font-family: inherit; }
.ab-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.ab-hero { text-align: center; margin-bottom: 1.6rem; }
.ab-hero h1 { font-size: clamp(1.4rem, 4.5vw, 1.9rem); }
.ab-hero p { color: var(--ink-soft); margin-top: 0.45rem; font-size: 0.86rem; }
.ab-lanes { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--line); border: 1px solid var(--line); }
@media (max-width: 700px) { .ab-lanes { grid-template-columns: 1fr; } }
.ab-lane { background: var(--paper); padding: 1.1rem 1rem 1.4rem; border-top: 3px solid transparent; }
.ab-lane.bad { border-top-color: var(--bad); }
.ab-lane.good { border-top-color: var(--good); }
.ab-lane-head { text-align: center; margin-bottom: 0.9rem; }
.ab-lane-head h2 { font-size: 0.98rem; }
.ab-lane.bad .ab-lane-head h2 { color: var(--bad); }
.ab-lane.good .ab-lane-head h2 { color: var(--good); }
.ab-code { display: block; font-family: 'IBM Plex Mono', monospace; font-size: 0.63rem; color: var(--ink-soft); margin-top: 0.5rem; white-space: pre-wrap; text-align: left; border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.5rem 0.6rem; line-height: 1.55; }
/* timeline */
.ab-timeline { margin-top: 2.6rem; position: relative; height: 46px; }
.ab-timeline-track { position: absolute; left: 0; right: 0; top: 18px; height: 10px; background: var(--paper-alt); border: 1px solid var(--line); overflow: hidden; }
.ab-timeline-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0%; background: var(--accent); transition: width linear; }
.ab-pin { position: absolute; top: -46px; display: flex; flex-direction: column; align-items: center; opacity: 0; transition: opacity 0.3s ease; z-index: 2; }
.ab-pin.is-shown { opacity: 1; }
.ab-pin.pin-left { left: 0; align-items: flex-start; }
.ab-pin.pin-right { left: auto; right: 0; align-items: flex-end; }
.ab-pin-dot { width: 10px; height: 10px; border-radius: 50%; border: 2px solid var(--paper); margin-bottom: 0.3rem; }
.ab-pin.bad-pin .ab-pin-dot { background: var(--bad); }
.ab-pin.good-pin .ab-pin-dot { background: var(--good); }
.ab-pin.ghost-pin .ab-pin-dot { background: var(--ink-soft); opacity: 0.6; }
.ab-pin-label {
font-size: 0.68rem; font-weight: 600; color: var(--ink-soft);
background: var(--paper-alt); border: 1px solid var(--line);
border-radius: 4px; padding: 0.25rem 0.5rem;
white-space: normal; text-align: center; max-width: 140px; line-height: 1.3;
}
.ab-pin.bad-pin .ab-pin-label { color: var(--bad); border-color: var(--bad); background: var(--bad-soft); }
.ab-pin.good-pin .ab-pin-label { color: var(--good); border-color: var(--good); background: var(--good-soft); }
.ab-pin.ghost-pin .ab-pin-label { color: var(--ink-soft); }
.ab-play { display: block; margin: 1.4rem auto 0; border: none; background: var(--accent); color: #fff; padding: 0.5rem 1.2rem; font-size: 0.8rem; font-weight: 600; cursor: pointer; }
.ab-play:active { transform: scale(0.96); }
.ab-play:disabled { opacity: 0.45; cursor: default; }
.ab-console {
margin: 1rem auto 0; max-width: 280px; text-align: left;
border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.5rem 0.75rem;
font-family: 'IBM Plex Mono', monospace; font-size: 0.7rem; min-height: 2.2em;
}
.ab-console .ab-cline { opacity: 0; transition: opacity 0.25s ease; }
.ab-console .ab-cline.is-shown { opacity: 1; }
.ab-lane.bad .ab-console .ab-cline.is-shown { color: var(--bad); }
.ab-lane.good .ab-console .ab-cline.is-shown { color: var(--good); }
.ab-note { text-align: center; font-size: 0.8rem; color: var(--ink-soft); margin-top: 1.6rem; padding-top: 1.1rem; border-top: 1px dashed var(--line); }
.ab-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-asyncbug *, .infographic-asyncbug *::before, .infographic-asyncbug *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="ab-wrap">
<div class="ab-lanes">
<!-- Broken -->
<div class="ab-lane bad">
<div class="ab-lane-head">
<h2>✕ Returns before the data exists</h2>
<code class="ab-code">function getUsers() {
let users = [];
setTimeout(() => { users = [...] }, 1000);
return users; // fires immediately
}</code>
</div>
<div class="ab-timeline">
<div class="ab-timeline-track"><div class="ab-timeline-fill" id="badFill"></div></div>
<div class="ab-pin bad-pin pin-left" id="badReturnPin">
<div class="ab-pin-dot"></div>
<div class="ab-pin-label">return [ ] — right away</div>
</div>
<div class="ab-pin ghost-pin pin-right" id="badDataPin">
<div class="ab-pin-dot"></div>
<div class="ab-pin-label">data ready — too late, nobody's listening</div>
</div>
</div>
<button class="ab-play" data-lane="bad" type="button">findUser("john")</button>
<div class="ab-console">
<div class="ab-cline" id="badLog">undefined</div>
</div>
</div>
<!-- Fixed -->
<div class="ab-lane good">
<div class="ab-lane-head">
<h2>✓ Waits, via a callback</h2>
<code class="ab-code">function getUsers(callback) {
setTimeout(() => {
callback([...]); // fires when ready
}, 1000);
}</code>
</div>
<div class="ab-timeline">
<div class="ab-timeline-track"><div class="ab-timeline-fill" id="goodFill"></div></div>
<div class="ab-pin good-pin pin-right" id="goodDataPin">
<div class="ab-pin-dot"></div>
<div class="ab-pin-label">callback fires — data really is ready</div>
</div>
</div>
<button class="ab-play" data-lane="good" type="button">findUser("john", console.log)</button>
<div class="ab-console">
<div class="ab-cline" id="goodLog">{ username: "john", email: "john@test.com" }</div>
</div>
</div>
</div>
<p class="ab-note">Both start a 1-second timer. The broken version answers <strong>before</strong> the timer finishes — with nothing. The callback version only answers <strong>when</strong> the timer actually finishes.</p>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-asyncbug')
|| document.querySelector('.infographic-asyncbug');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') { dark = true; }
else if (bodyAttr === 'false') { dark = false; }
else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
const DUR = 1400;
function resetFill(fill) {
fill.style.transition = 'none';
fill.style.width = '0%';
void fill.offsetWidth;
}
function growFill(fill, ms) {
fill.style.transition = 'width ' + ms + 'ms linear';
requestAnimationFrame(() => { fill.style.width = '100%'; });
}
function playBad() {
const btn = wrapper.querySelector('.ab-play[data-lane="bad"]');
const fill = wrapper.querySelector('#badFill');
const returnPin = wrapper.querySelector('#badReturnPin');
const dataPin = wrapper.querySelector('#badDataPin');
const log = wrapper.querySelector('#badLog');
btn.disabled = true;
resetFill(fill);
returnPin.classList.remove('is-shown');
dataPin.classList.remove('is-shown');
log.classList.remove('is-shown');
growFill(fill, DUR);
setTimeout(() => {
returnPin.classList.add('is-shown');
log.classList.add('is-shown');
}, 80);
setTimeout(() => {
dataPin.classList.add('is-shown');
btn.disabled = false;
}, DUR);
}
function playGood() {
const btn = wrapper.querySelector('.ab-play[data-lane="good"]');
const fill = wrapper.querySelector('#goodFill');
const dataPin = wrapper.querySelector('#goodDataPin');
const log = wrapper.querySelector('#goodLog');
btn.disabled = true;
resetFill(fill);
dataPin.classList.remove('is-shown');
log.classList.remove('is-shown');
growFill(fill, DUR);
setTimeout(() => {
dataPin.classList.add('is-shown');
log.classList.add('is-shown');
btn.disabled = false;
}, DUR);
}
wrapper.querySelectorAll('.ab-play').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.dataset.lane === 'bad') playBad();
else playGood();
});
});
})();
</script>🎯 Practical Example with Fetch API
❌ Wrong Approach (returns empty array)
function getUsers() {
let users = [];
fetch("<https://jsonplaceholder.typicode.com/users>")
.then((res) => res)
.then((data) => users.push(data));
console.log(users);
}fetch() is async → data arrives later → returns [].
✅ Correct Solution (Using Callback)
function getUsers(callback) {
fetch("<https://jsonplaceholder.typicode.com/users>")
.then((res) => res.json())
.then((data) => callback(data));
}
// Usage
getUsers((users) => {
console.log(users);
});fetch()is asynchronous, so we can't return the data directly.Instead, we wait for the fetch to finish and then call the callback with the user data.
The callback receives the actual users and prints them.