Pending
The operation has started but has not settled. Neither success nor failure has happened yet.
Async code does not have to be hard to follow. Here is how promises, async and await fit together in real projects.
JavaScript runs on a single thread, so long operations such as reading a file, calling an API or querying a database cannot block the page. Instead they run in the background and hand back a result when they finish. The awkward part has always been how that result is delivered without turning code into a puzzle.
Modern code uses promises for that delivery and two keywords, async and await, to make promise-based code read like ordinary step-by-step instructions.
The original approach passed a function to be called when work finished. One callback is fine, but when each step depends on the previous one, callbacks nest inside callbacks. The code marches to the right, error handling is duplicated at every level, and reading the sequence of steps becomes difficult.
Callbacks are not wrong, but they make dependency chains and shared error handling hard to review. Promises were introduced to give that chain a single, composable shape.
A promise is an object that represents a value you do not have yet. It is always in one of three states, and it only moves forward.
The operation has started but has not settled. Neither success nor failure has happened yet.
The operation finished successfully and produced a value that the promise now holds.
The operation failed and carries a reason, usually an Error describing what went wrong.
Because a promise is a plain value, you can pass it around, return it from a function and combine several of them before awaiting the result.
Marking a function async makes it return a promise automatically. Inside it you can use await to pause until another promise settles and then continue with its value. The code below reads top to bottom even though the work is asynchronous.
async function loadUser(id) {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json();
}
The await keyword does not block the whole program. It only pauses this one function, so other work can continue while the network request is in flight.
A rejected promise turns into a thrown error at the await point. That means ordinary try and catch blocks handle asynchronous failures, which keeps error handling in one place instead of scattered through callbacks.
async function showUser(id) {
try {
const user = await loadUser(id);
render(user);
} catch (error) {
console.error("Could not load the user:", error);
showMessage("Something went wrong. Please try again.");
}
}
Awaiting one promise after another makes independent tasks run one at a time. When the tasks do not depend on each other, start them together and await them as a group with Promise.all, which resolves when every promise resolves.
const [profile, orders] = await Promise.all([
loadUser(id),
loadOrders(id),
]);
renderDashboard(profile, orders);
If any promise in the group rejects, the whole call rejects, so wrap it in the same try and catch pattern you already use.
Forgetting await is the most frequent slip. Without it you keep the promise object instead of the value, so logging it shows Promise { <pending> } and later code runs before the data exists.
Another trap is awaiting inside a loop over independent tasks, which quietly makes them sequential. Collect the promises first and await them together when order does not matter. If order does matter, a plain for loop with await is a deliberate and readable choice.
Finally, remember that await only works inside an async function or at the top level of a module. Keeping functions small and awaited exactly once makes asynchronous code as easy to follow as the synchronous kind.