JavaScript Promises and Async/Await Explained
SkillVeris Team
Engineering Team

A JavaScript Promise is an object representing the eventual result of an asynchronous operation, ending in one of three states: pending, fulfilled, or rejected.
In this guide, you'll learn:
- You handle a Promise with .then for success and .catch for errors, or more cleanly with async/await syntax.
- async/await is syntactic sugar over Promises — await pauses a function until a Promise settles, making async code read like ordinary sequential code.
- Wrap await calls in try/catch to handle rejected Promises the same way you handle thrown errors.
- Promise.all runs multiple Promises concurrently and resolves when all succeed, which is faster than awaiting them one by one.
1What Is a JavaScript Promise?
A JavaScript Promise is an object that represents a value that is not available yet but will be at some point — the result of an asynchronous operation like a network request or a timer. A Promise is always in one of three states: pending while the work is ongoing, fulfilled when it succeeds with a value, or rejected when it fails with an error.
Promises exist because JavaScript is single-threaded. Rather than freeze the page while waiting for slow work, a Promise lets you register callbacks that run later, once the result settles, keeping the interface responsive.
2Handling Promises With then and catch
The traditional way to consume a Promise is with .then and .catch. The function passed to .then receives the fulfilled value; the one passed to .catch receives the rejection reason. Because .then returns a new Promise, you can chain steps together, each handing its result to the next.
- fetch('/api/user')
- .then(response => response.json()) // returns another Promise
- .then(user => console.log(user.name)) // runs after json resolves
- .catch(error => console.error(error)) // catches any step failing
- .finally(() => console.log('done')) // always runs
💡Chaining Rule
Return the Promise inside a .then callback so the next .then waits for it. Forgetting the return is the most common source of chains that run out of order.
3Cleaner Code With async/await
async/await is modern syntax layered on top of Promises. Mark a function async and you can use await inside it to pause until a Promise settles, then continue with its resolved value as if it were returned synchronously. The code reads top to bottom, which is far easier to follow than nested .then chains.
Under the hood nothing changes — await simply waits for the same Promise. It is a readability upgrade, not a new mechanism.
- async function loadUser() {
- const response = await fetch('/api/user'); // waits here
- const user = await response.json(); // then waits here
- return user.name; // resolves the returned Promise
- }
4Handling Errors With try/catch
With async/await, a rejected Promise behaves like a thrown error, so you catch it with a normal try/catch block. This unifies error handling: synchronous exceptions and async failures are caught the same way, which is one of the biggest ergonomic wins over .catch chains.
- async function loadUser() {
- try {
- const response = await fetch('/api/user');
- if (!response.ok) throw new Error('Request failed');
- return await response.json();
- } catch (error) {
- console.error('Could not load user:', error);
- }
- }
⚠️fetch Does Not Reject on 404
The fetch Promise only rejects on network failure, not on HTTP error status codes. Always check response.ok yourself and throw if it is false, or bad responses slip through silently.
5Running Promises Concurrently
Awaiting Promises one after another makes them run in sequence, which wastes time when they are independent. Promise.all launches them together and resolves once all have fulfilled, collapsing the total wait to that of the slowest one. Related helpers cover other patterns.
- const [user, posts] = await Promise.all([fetchUser(), fetchPosts()]); // concurrent
- Promise.allSettled([...]) // waits for all, never short-circuits on rejection
- Promise.race([...]) // resolves or rejects with the first to settle
- Promise.any([...]) // resolves with the first fulfilled, ignoring rejections
Sequential vs Concurrent
Two requests that each take one second run in two seconds when awaited in a row, but about one second inside Promise.all. Use Promise.all whenever the operations do not depend on one another.
6Async Functions Always Return Promises
Every async function returns a Promise, no matter what you write inside it. If you return a plain value, it is wrapped in a resolved Promise; if you throw, the Promise rejects. This means the caller of an async function must await it or attach .then — you cannot use the return value directly as if it were synchronous.
- async function getNumber() { return 42; } // returns Promise<number>
- const n = getNumber(); // n is a Promise, not 42
- const value = await getNumber(); // value is 42
- getNumber().then(v => console.log(v)); // also works
7Best Practices
A few habits keep async JavaScript predictable and free of the subtle bugs that come from unhandled rejections or accidental serial execution.
- Always handle rejections — with try/catch around await, or a .catch on the chain.
- Use Promise.all for independent operations instead of awaiting them one by one.
- Check response.ok after fetch; it does not reject on HTTP error codes.
- Avoid await inside a plain forEach loop — it does not wait; use a for...of loop.
- Do not mix .then chains and await in the same function; pick one style for clarity.
8Key Takeaways
The essentials of Promises and async/await come down to these points.
- A Promise represents a future value and settles as fulfilled or rejected.
- async/await is cleaner syntax over Promises that reads sequentially.
- Handle async errors with try/catch around your await calls.
- Run independent Promises concurrently with Promise.all to save time.
- Every async function returns a Promise, so its result must be awaited or handled.
9Frequently Asked Questions
Q: What is the difference between a Promise and async/await? A: They are two ways to work with the same thing. A Promise is the underlying object representing a future value, handled with .then and .catch. async/await is syntax that lets you consume Promises in code that reads top to bottom, but it produces and awaits the very same Promises.
Q: Does await block the whole page? A: No. await only pauses the async function it sits in; the rest of your program, including the UI, keeps running. That is the point of asynchronous code — the single JavaScript thread stays free to handle other work while the awaited operation completes.
Q: Why is my async function returning a Promise instead of a value? A: Because async functions always wrap their return value in a Promise. To get the underlying value, await the function call inside another async function, or attach a .then handler. You cannot read the value synchronously.
Q: When should I use Promise.all? A: Use Promise.all when you have several independent asynchronous operations and want them to run concurrently rather than one after another. It resolves once all have fulfilled and rejects immediately if any one fails, so use Promise.allSettled if you need every result regardless of failures.
Related Reading
Get The Print Version
Download a PDF of this article for offline reading.
About the Publisher
SkillVeris Team
Engineering Team
Our engineering writers turn abstract code concepts into hands-on, project-driven learning experiences.
View all postsRelated Posts
Never miss an update
Get the latest tutorials and guides delivered to your inbox.
No spam. Unsubscribe anytime.