Article
JavaScript Promises and Async/Await Explained for Beginners
If you write JavaScript for the web, you deal with asynchronous code constantly. Fetching data from an API, reading files in Node.js, waiting for a timer or uploading an image all take time, and JavaScript does not stop everything while it waits. Instead, it continues running other code and comes back when the result is ready.
For beginners, this is one of the most confusing parts of the language. Code seems to run in the wrong order, variables are undefined when you expect data and errors disappear silently. Promises and async/await are the modern tools for handling all of this. This guide explains them in plain terms with the habits that prevent common bugs.
Why Asynchronous Code Exists
JavaScript in the browser runs on a single main thread that also handles user interaction and page updates. If it froze every time it waited for a network request, the whole page would lock up. So slow operations are started, and JavaScript is notified when they finish.
Older code handled this with callbacks, functions passed in to run later. Callbacks work, but nesting many of them leads to code that is hard to read and harder to handle errors in. Promises were introduced to make this cleaner.
Under the hood, JavaScript uses an event loop. Finished operations place their follow-up work in a queue, and the engine processes that queue whenever the main thread is free. You do not need to master the details to write good code, but knowing the queue exists explains why results arrive later than the surrounding lines.
What a Promise Is
A promise is an object representing a value that will be available later. It can be in one of three states:
- Pending: the operation has not finished yet
- Fulfilled: the operation succeeded and the promise has a value
- Rejected: the operation failed and the promise has an error
You attach handlers with then for success and catch for errors. Many browser and Node.js APIs, such as fetch, return promises. Once a promise is fulfilled or rejected, its state does not change, which makes behaviour predictable.
Async/Await: Cleaner Syntax
Async/await is syntax built on top of promises. Marking a function async means it always returns a promise. Inside it, the await keyword pauses that function until a promise settles, then gives you the result, while the rest of the program keeps running.
This lets asynchronous code read almost like normal top-to-bottom code. Instead of chaining several then calls, you write one line per step: await the request, await the JSON parsing, then use the data. It is easier to read, debug and review, which is why most modern codebases prefer it.
Remember that await only pauses the async function it appears in. Code that called that function keeps running unless it also awaits the result. This is why async behaviour tends to spread upwards through a codebase, with callers becoming async too.
Handling Errors Properly
Unhandled errors are the biggest source of async bugs. With async/await, wrap awaited calls in try and catch blocks where you can respond meaningfully, such as showing a message or retrying. A few rules help:
- Always handle rejections somewhere, either with try/catch or a catch handler.
- Remember that fetch does not reject for HTTP errors like 404 or 500, so check the response status yourself.
- Show users a helpful message instead of failing silently.
- Log errors with enough context to debug them later.
- Use finally for cleanup, such as hiding a loading spinner.
Our guide on error handling in web apps explains how to make failures graceful.
Tip: a forgotten await is one of the most common bugs. If a variable unexpectedly contains a promise instead of data, check whether you awaited it.
Sequential vs Parallel
Awaiting one call after another runs them in sequence. That is correct when each step depends on the previous result, but wasteful when tasks are independent. Loading a user profile, their orders and notifications separately can take three times longer than necessary.
For independent tasks, start them together and wait for all of them with Promise.all. If one fails, Promise.all rejects, so use Promise.allSettled when you want results from every task regardless of individual failures. Be careful with very large numbers of parallel requests, since they can overload APIs or hit rate limits. Process big lists in small batches instead.
Avoid using await inside forEach loops, because forEach does not wait for async callbacks. Use a regular for...of loop for sequential processing, or map the items to promises and use Promise.all for parallel work.
FAQ
Is async/await faster than promises?
No. Async/await is built on promises and performs similarly. It mainly improves readability and error handling.
Can I use await at the top level of a file?
In modern JavaScript modules, yes, top-level await is supported. In older scripts or some environments, you need to put await inside an async function.
Why does my code run before my data arrives?
Code after an async call continues immediately unless it is inside the same async function after an await. Move dependent logic after the awaited result.
Asynchronous code becomes much less mysterious once you understand promises. Use async/await for readable steps, handle every error, run independent tasks in parallel and watch for missing awaits, and your JavaScript will behave the way you expect.