Promise vs Async Await: Code Examples for Beginners

JavaScriptPromise vs Async Await: Code Examples for Beginners

Think async/await makes Promises useless?
Not true.
Async/await does make code read like a recipe and is easier to debug.
Promises are the building blocks under the hood, and you still need them for things like parallel tasks.
In this post we’ll explain both with simple, working code examples so you can see the difference, handle errors, and choose the right pattern for beginner projects.
By the end you’ll know enough Promises to understand async/await and when to use Promise.all for parallel work.

Quick Verdict: Which Approach Fits Beginners Best in Promise vs Async Await

Kbw0lazPVY-w7IClG6MS3A

Async/await wins for readability and learning momentum. If you’re building projects where you fetch data, wait for user input, or chain multiple operations, async/await reads like a step-by-step recipe and makes debugging easier because your code flows top to bottom. Promises still power everything under the hood, but async/await hides the chaining syntax so you can focus on what your code does, not how to wire callbacks together.

That said, you need to understand Promises first. Async/await is syntactic sugar built on Promises. If you skip learning how resolve and reject work, you’ll hit confusing bugs when error handling breaks or when you try to run tasks in parallel. Think of Promises as the foundation and async/await as the cleaner tool you use after you understand the foundation. Most beginners should learn enough about Promises to grasp .then() and .catch(), then switch to async/await for daily project work.

Here’s the quick breakdown of key differences:

Syntax style. Promises use .then() chains, async/await uses await assignments that look like synchronous code.

Error handling. Promises rely on .catch() at the end of the chain, async/await uses try/catch blocks around await calls.

Readability. Async/await reduces nested callbacks and makes code easier to scan. Promise chains can get messy when you have three or more steps.

Learning curve. Promises require understanding resolve/reject states and method chaining. Async/await feels more natural if you already know how to write normal functions.

Parallel execution. Promises offer Promise.all for running multiple tasks at once. Async/await requires explicitly calling Promise.all or awaiting each task in sequence.

Understanding Promises in JavaScript: The Foundation Behind Async Await

Bd5PcT0GUvOfP8No8kdGDg

A Promise is an object that represents a future value. When you call a function that takes time (like fetching data from a server), instead of waiting and blocking everything else, JavaScript gives you a Promise immediately. That Promise sits in a “pending” state until the operation finishes. Once done, the Promise either “fulfills” with a result or “rejects” with an error. You attach .then() to handle the success case and .catch() to handle the failure case.

Promises solve the problem of nested callbacks by letting you chain operations in a flat structure. Instead of passing a callback function into another callback function (callback hell), you return a Promise from each step and attach the next step with another .then(). Each .then() receives the value from the previous step, processes it, and passes the result forward. If any step fails, the error skips all remaining .then() calls and jumps straight to the first .catch() it finds.

Here’s how typical Promise code looks in practice.

Creating a simple Promise. new Promise((resolve, reject) => { setTimeout(() => resolve("Done"), 1000); }); creates a Promise that resolves after one second.

Chaining with .then(). fetch("https://api.example.com/user").then(response => response.json()).then(data => console.log(data)); shows two .then() calls in sequence: first to parse the response, second to log the parsed data.

Handling errors with .catch(). fetch("https://api.example.com/user").then(response => response.json()).catch(err => console.error("Failed:", err)); catches any error from either the fetch or the JSON parsing step.

Understanding these three patterns (creating, chaining, catching) is essential because async/await simply rewrites them in a way that looks like normal function calls. When you write await fetch(url), JavaScript is still creating and resolving a Promise behind the scenes. You’re just using cleaner syntax to do it.

Async Await Syntax Explained for Beginners: How It Changes Promise-Based Code

xTgb48xoUpabDU6S1gFqoA

The async Keyword

The async keyword goes before a function declaration or expression. When you mark a function as async, JavaScript automatically wraps the return value in a Promise. Even if you write return 42; inside an async function, JavaScript turns that into return Promise.resolve(42); for you. That means every async function always returns a Promise, whether you use await inside it or not.

You can apply async to regular function declarations like async function getData() { }, arrow functions like const getData = async () => { }, or even immediately invoked function expressions like (async () => { })();. The key rule is this: if you want to use await anywhere inside a function, that function must be marked async.

The await Keyword

The await keyword pauses execution of the async function until the Promise it’s waiting on settles (either resolves or rejects). When you write const response = await fetch(url);, JavaScript stops at that line, waits for the fetch to complete, then assigns the resolved value to response. The code below the await line won’t run until the Promise finishes. This makes asynchronous code read like synchronous code, line by line, top to bottom.

Await only works with Promises. If you await a non-Promise value like await 42, JavaScript immediately wraps it in a resolved Promise and continues. You can only use await inside an async function or at the top level of a module. If you try to use await in a regular function, you’ll get a syntax error.

Here are the key syntax rules.

Always mark the function async. async function run() { await doSomething(); } works. function run() { await doSomething(); } throws an error.

Await pauses until resolution. const result = await fetchData(); waits for fetchData() to finish before moving to the next line.

Wrap await in try/catch for errors. try { const data = await fetchData(); } catch (err) { console.error(err); } handles rejections.

You can await multiple times in sequence. const user = await getUser(); const posts = await getPosts(user.id); runs each step one after the other.

Now compare a Promise chain to async/await doing the same work. With Promises, you might write fetch(url).then(response => response.json()).then(data => console.log(data)).catch(err => console.error(err));. With async/await, the same code becomes async function run() { try { const response = await fetch(url); const data = await response.json(); console.log(data); } catch (err) { console.error(err); } }. Both do the exact same thing under the hood: fetch data, parse JSON, log the result, handle errors. The async/await version just looks like normal step-by-step code instead of method chaining.

Code Readability in Promise vs Async Await: A Side-by-Side Comparison

GAXcWPWmUUisZnU41zjRjQ

The main reason developers prefer async/await is readability. When you chain multiple asynchronous operations together, Promise chains stack .then() calls horizontally. Each .then() adds another layer of indentation if you’re not careful, and it’s easy to lose track of which callback belongs to which step. Async/await lets you write the same sequence vertically, using normal variable assignments and standard control flow like if statements and loops.

Let’s walk through the same task twice: fetching a user’s details, then fetching the user’s location based on those details, then fetching weather data for that location. Both examples simulate API calls using Promises that resolve after a short delay.

Using Promises

Here’s the Promise-based version. We chain three .then() calls, one for each step:

function getWeather() {
  fetchUser()
    .then(user => {
      return fetchLocation(user.id);
    })
    .then(location => {
      return fetchWeather(location.city);
    })
    .then(weather => {
      console.log("Weather:", weather);
    })
    .catch(err => {
      console.error("Something broke:", err);
    });
}

Each .then() must return the next Promise so the chain continues. If you forget to return, the next .then() receives undefined. Error handling happens in one .catch() at the end, which is convenient but can make it harder to trace which step caused the failure.

Using Async/Await

Now the same task with async/await:

async function getWeather() {
  try {
    const user = await fetchUser();
    const location = await fetchLocation(user.id);
    const weather = await fetchWeather(location.city);
    console.log("Weather:", weather);
  } catch (err) {
    console.error("Something broke:", err);
  }
}

You read it like a normal function: fetch the user, then fetch the location, then fetch the weather, then log the result. If any step fails, the catch block handles it. No need to worry about returning Promises or chaining methods. The code flows in the order you expect.

Factor Promises Async/Await
Lines of Code More horizontal chaining Fewer lines, more vertical
Error Handling Style .catch() at the end try/catch around await
Readability Harder with 3+ steps Reads like synchronous code
Typical Beginner Pitfalls Forgetting to return Promises Forgetting to mark function async

Error Handling Differences in Promise vs Async Await

mC_WGT5sWma7RGFLt4q10w

Promises handle errors with .catch(). When any Promise in a chain rejects, JavaScript skips all remaining .then() calls and jumps to the first .catch() it finds. That .catch() receives the error as its argument. You can attach .catch() at the end of the chain to catch all errors, or you can insert .catch() in the middle to handle specific steps and continue the chain. If you don’t attach a .catch() anywhere, you get an “unhandled Promise rejection” warning in the console.

Async/await uses try/catch blocks instead. You wrap your await statements inside a try block, and if any await rejects, execution jumps to the catch block with the error. This feels more natural if you’re used to regular error handling in JavaScript. Try/catch also lets you add a finally block that runs whether the operation succeeds or fails, which is useful for cleanup tasks like closing connections or hiding loading spinners.

One key difference: with Promises, you can handle errors for individual steps by chaining multiple .catch() calls. With async/await, a single catch block typically handles all errors in the function unless you nest multiple try/catch blocks. For beginners, a single try/catch is simpler and easier to reason about. Just remember that if you forget the try/catch entirely, your async function will return a rejected Promise and the error will bubble up to whoever called that function.

Here are the top beginner mistakes when handling async errors.

Forgetting try/catch around await. If you don’t wrap await in try/catch, rejected Promises crash your code silently or cause unhandled rejection warnings.

Mixing .catch() with async/await. Some beginners add .catch() to individual await lines like await fetchData().catch(err => {...}). It works, but it’s inconsistent. Pick one style and stick with it.

Not re-throwing errors when needed. If you catch an error but the calling function needs to know about it, you must throw it again with throw err; inside the catch block.

Awaiting inside a .catch() callback. You can’t use await inside a regular .catch() unless you wrap it in an async function. This catches people by surprise when converting Promise code.

Performance and Execution Flow in Promise vs Async Await

srO0XmBBUKKgZyiERoCVtQ

Async/await doesn’t change how JavaScript runs asynchronous code. Under the hood, await is still using Promises and the event loop. The performance difference between a Promise chain and an async/await version of the same task is negligible. However, the way you structure your await calls can create accidental performance problems. If you await tasks one by one when they could run in parallel, you’re slowing down your code unnecessarily.

When you write await taskA(); followed by await taskB();, JavaScript waits for taskA to finish completely before starting taskB. That’s fine if taskB depends on the result of taskA. But if taskA and taskB are independent (like fetching two different API endpoints), you should start them both at once using Promise.all. Instead of writing const a = await fetchA(); const b = await fetchB(); (which takes the sum of both times), write const [a, b] = await Promise.all([fetchA(), fetchB()]); (which takes only as long as the slower task).

Promises give you more explicit control over parallel execution. Methods like Promise.all, Promise.race, and Promise.allSettled let you run multiple Promises concurrently and handle their results in different ways. Async/await requires you to intentionally call these methods. If you forget, you default to sequential execution, which feels natural when reading the code but can hurt performance.

Here are three common execution patterns.

Sequential with await. const user = await getUser(); const posts = await getPosts(user.id); waits for the user before fetching posts. Use this when the second task needs data from the first.

Parallel with Promise.all. const [user, settings] = await Promise.all([getUser(), getSettings()]); starts both tasks at the same time and waits for both to finish. Use this when tasks are independent.

Race with Promise.race. const fastest = await Promise.race([fetchFromCDN(), fetchFromBackup()]); returns whichever Promise resolves first. Use this for fallback scenarios or timeouts.

Pattern Sequential or Parallel? Common Use Case
await task1(); await task2(); Sequential task2 depends on task1’s result
await Promise.all([task1(), task2()]) Parallel Both tasks are independent
await Promise.race([task1(), task2()]) Parallel (returns first) Fallback or timeout scenarios

Common Beginner Mistakes in Promise vs Async Await

SwOq3zVPUsWdKqQByWAFpA

The most common mistake beginners make is using await outside an async function. JavaScript will throw a syntax error immediately. You must mark the enclosing function with the async keyword before you can use await inside it. If you need to run async code at the top level (like in a script file), wrap it in an async IIFE: (async () => { await doSomething(); })();. That creates an anonymous async function and calls it right away.

Another frequent error is forgetting to await a Promise. If you write const response = fetch(url); without await, response won’t be the data. It’ll be a Promise object. Your next line might try to call response.json() and fail because response is still pending. Always put await in front of any function that returns a Promise when you need the resolved value immediately.

Here are six mistakes and how to fix them.

Using await in a non-async function. Fix: add async before the function declaration, like async function run() { await doSomething(); }.

Forgetting to await a Promise. Fix: write const result = await fetchData(); instead of const result = fetchData();.

Awaiting non-Promise values unnecessarily. Fix: remove await if the function doesn’t return a Promise. It won’t break anything, but it’s redundant and confusing.

Creating sequential awaits when parallel is better. Fix: use await Promise.all([task1(), task2()]) instead of awaiting each task separately.

Not handling rejected Promises. Fix: wrap await calls in try/catch or attach .catch() to the Promise.

Mixing callbacks and async/await. Fix: convert callback-based APIs to Promises first, or use util.promisify in Node.js, then await them. Don’t mix await with callback-style function(err, data) patterns in the same flow.

Practical Guide for Choosing Between Promises and Async Await

Zkxv4vL_WfqN6dQ80Zjg0w

Most projects benefit from using async/await for day-to-day asynchronous code. The syntax is cleaner, errors are easier to trace, and the flow matches how you think about the task. If you’re fetching data, processing it step by step, and displaying results, async/await keeps everything readable. It also makes conditional logic and loops simpler because you can use normal if statements and for loops without wrapping them in Promise chains.

However, Promises still shine in specific scenarios. When you need fine-grained control over multiple concurrent operations (like starting several tasks at once, handling each result individually as it completes, or racing multiple sources to pick the fastest), Promise.all, Promise.allSettled, and Promise.race give you that control explicitly. Async/await requires you to call these methods anyway, so there’s no syntax advantage. In fact, chaining .then() can sometimes be clearer for very short, one-off operations where creating a full async function feels like overkill.

Understanding when to choose each approach comes down to readability, complexity, and whether you need parallelism. For sequential steps, async/await wins. For parallel operations, you still need Promise methods, but you can await them. For simple one-step operations, either works. Pick whichever feels more natural.

Here’s when Promises work best.

Many parallel tasks with individual handling. Use Promise.allSettled when you need to see the result of each Promise separately, even if some reject.

Short utility functions. A single fetch().then().catch() might be clearer than defining an async function.

Library APIs that return Promises. Some APIs are designed around Promise chains (like certain middleware or chaining patterns). Stick with .then() if converting to async/await adds complexity.

Race conditions or timeouts. Promise.race is more explicit than await with a timeout wrapper.

And here’s when async/await is better.

Multi-step workflows. Fetching data, transforming it, and saving results is easier with await assignments than chaining.

Error handling in complex flows. Try/catch blocks are simpler than multiple .catch() handlers when you have branching logic.

Loops and conditionals. Writing for (const id of ids) { await process(id); } is more readable than building a Promise chain inside a loop.

Debugging. Stack traces from async/await are clearer than nested .then() calls, making it easier to find where errors occur.

Goal Recommended Approach
Run three API calls in sequence Async/await with three await statements
Run three API calls in parallel await Promise.all([call1(), call2(), call3()])

Use this checklist to decide which style fits your code.

Do I need to run tasks in parallel? If yes, use Promise.all or Promise.race explicitly. Async/await defaults to sequential unless you call these methods.

Is my code more than two steps? If yes, async/await will be more readable than chaining .then() calls.

Am I working with loops or conditionals? If yes, async/await makes that logic simpler because you can use normal for loops and if statements without wrapping everything in Promise callbacks.

Final Words

You followed a weather-app flow: built Promises with .then/.catch and then rewrote the steps using async/await and try/catch.

We compared readability, error handling, and performance, and flagged common beginner mistakes. Small code wins make this approachable.

This post had promise vs async await explained with code examples for beginners, so you can try both and pick what fits your project. Try a tiny project and you’ll build confidence fast. Nice work.

FAQ

Q: Which approach fits beginners best in Promise vs Async Await?

A: The best approach for beginners is async/await because it makes async code read like normal, linear code; use Promises directly when you need fine control or many parallel tasks.

Q: What is a Promise in JavaScript and why learn it before async/await?

A: A Promise in JavaScript is an object representing a future value with states (pending, fulfilled, rejected); learning it first shows how resolve/reject, .then() and .catch() work under async/await.

Q: How does async/await change Promise-based code?

A: Async/await changes Promise-based code by letting you mark functions with async and pause with await, producing linear flow and allowing try/catch for clearer error handling instead of chained .then().

Q: How do error handling differ between Promises and async/await?

A: Error handling differs: Promises use .catch() chains, while async/await uses try/catch blocks; try/catch usually centralizes errors, but Promise.all still fails fast on the first rejection.

Q: When should I use Promise.all or parallel operations?

A: You should use Promise.all for independent tasks you can run in parallel to speed things up; avoid awaiting each task in a loop when parallelism is wanted.

Q: What are common beginner mistakes with Promises and async/await and how to fix them?

A: Common beginner mistakes include using await outside async, forgetting to await, mixing callback styles, and unhandled rejections; fix by marking functions async, awaiting results, converting callbacks, and adding try/catch or .catch().

Q: How do async functions return values compared to Promises?

A: Async functions return a Promise that resolves to the returned value; to access that value use await inside another async function or use .then() on the returned Promise.

Q: How do I convert callback code to Promises or async/await?

A: To convert callbacks, wrap the callback API in a new Promise that resolves/rejects, then consume it with .then() or await inside an async function for clearer flow.

Q: Can I mix Promises and async/await in the same codebase?

A: You can mix Promises and async/await since async/await is built on Promises; prefer consistency, and ensure Promise returns and errors are handled the same way across the code.

Check out our other content

Check out other tags: