⏳ javascript: async, promises & more
🤔 is javascript synchronous?
yes - javascript is a synchronous, single-threaded language, it runs one thing at a time, line by line.
but... we can mimic asynchronous behaviour using:
- callbacks
- promises
- async/await
these tools allow us to:
- wait for data (like from APIs)
- handle delays (like timers)
- avoid blocking the main thread
🪝 callbacks
a callback is a function passed into another function to be run later.
function fetchData(callback) {
setTimeout(() => {
callback('data received!');
},1000)
}
fetchData((result) => {
console.log(result);
});
🧠 but callbacks can get messy - known as "callback hell":
doThing(() => {
doNext() => {
doAnother(() =>{
...
})
}
})
💭 promises
a promise is javascript object that represents a value you don't have yet, but will get in the future (or maybe not).
it's like saying "i promise you something later"
🧱 promise has 3 possible states:
pending– still waitingfulfilled- got the valuerejected- something went wrong
you can .then() for success, .catch() for errors:
fetchData
.then((res) => console.log(res))
.catch((err) => console.log(err));
⚡ async / await
- syntactic sugar over promises 🚀
- ✅ makes async code look synchronous
- 📦 await can only be used inside an async function
async function getData() {
try {
const res = await fetchData();
console.log(res);
} catch (err) {
console.log(err);
}
}
what does Async functions return and why?
⚙️ async await is a syntax sugar for promises and whenever you write async in front of a function, it will basically wrap that code in a promise and return a promise that will resolve with the return of that function.
🧠 so whenever you have an async function, that function will return a promise that will end up resolving with whatever the return of that function is.
example:
import {useEffect, useState } from "react";
function Characters() {
const [people, setPeople ] = useState([]);
async function getPeople(){
try {
const data = await fetch("https://swapi.dev/api/people");
const response = await data.json();
setPeople(response.results);
} catch(err) {
console.log(err.message);
}
}
useEffect(() => {
getPeople();
}, [])
return <>
{people.map((character, index) => (
return {character.name}
)}
>
}
export default Characters;
🔄 Promise.all, Promise.race, Promise.allSettled
Promise.all
runs multiple promises in parallel, resolves when all succeed, rejects if any fail.
const [users, posts, comments] = await Promise.all([
fetch("/api/users").then(r => r.json()),
fetch("/api/posts").then(r => r.json()),
fetch("/api/comments").then(r => r.json()),
]);
Promise.allSettled
waits for all to finish (success or failure).
const results = await Promise.allSettled([
fetch("/api/users"),
fetch("/api/failing-endpoint"),
]);
results.forEach(r => {
if (r.status === "fulfilled") console.log(r.value);
else console.log(r.reason);
});
Promise.race
resolves/rejects with the first promise to settle.
🔁 event loop basics
javascript is single-threaded but handles async via the event loop.
console.log("1"); // sync — runs first
setTimeout(() => console.log("2"), 0); // macro task — runs last
Promise.resolve().then(() => console.log("3")); // microtask — runs second
console.log("4"); // sync
// output: 1, 4, 3, 2
key rule: microtasks (promises) always run before macro tasks (setTimeout).
⏱️ handling API timeouts
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeout);
return await response.json();
} catch (error) {
clearTimeout(timeout);
if (error.name === "AbortError") throw new Error("request timed out");
throw error;
}
}
🔁 retry logic
simple retry
async function fetchWithRetry(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (error) {
if (i === retries - 1) throw error;
}
}
}
retry with exponential backoff
async function fetchWithBackoff(url, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (error) {
if (i === retries - 1) throw error;
const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s...
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
What is a pure function and when do we use it?
A pure function is very useful when using functional programming. Why? Because it really does not modify anything else — it has no side effects. 🌱
You really just give it an input, and it gives you an output, and there is no other change to:
- any global state of the system 🌐,
- or any calls outside the system (like APIs, localStorage, DBs, etc.)
This is extremely good because they are very deterministic ✅ — so you always know:
- given an input → you always get the same output.
they're commonly used in functional programming and in most JavaScript codebases, because it makes things simpler, more predictable, and easier to test.