Intermediate35 minjavascriptasyncpromisesintermediate

Introduction to Async JavaScript

Learn why asynchronous JavaScript exists, how it works, and how Promises give you a clean way to handle code that takes time to complete.

Learning Objectives

By the end of this lesson, you'll be able to:

  • βœ“ Explain Explain what synchronous and asynchronous execution mean, and why the difference matters
  • βœ“ Describe Describe what a Promise is and what the three states of a Promise represent
  • βœ“ Use `.then()` and `.catch()` to handle Promise results and errors
  • βœ“ Read and interpret async code with confidence before writing it

Why This Matters:

Learn why asynchronous JavaScript exists, how it works, and how Promises give you a clean way to handle code that takes time to complete.

Before You Start:

You should be familiar with:

The problem: JavaScript only does one thing at a time

JavaScript is single-threaded. That means it has one call stack, and it processes one piece of code at a time, in order. This is fine for most tasks.

The problem arises when a task takes time β€” loading a file, querying a database, calling an API. If JavaScript ran those tasks synchronously, it would block everything else until they finished.

// Imaginary synchronous fetch β€” do NOT do this
const data = slowNetworkRequest(); // everything freezes here
console.log(data);                 // only runs after the wait

In a browser, this means the UI freezes. The user can't click, scroll, or type. A three-second network request becomes a three-second dead interface.

The solution is to hand off slow work to the browser's Web APIs, which run separately from the JavaScript call stack. When the work is done, the result is returned to JavaScript via a callback queue and processed when the stack is free. This is the event loop model.

You don't need to fully understand the event loop mechanics to write good async code β€” but knowing it exists helps explain why async code looks different from synchronous code.


Diagram of async JavaScript handing slow work to browser APIs and receiving results later.Call Stackclick handlerrender UIBrowser APIsnetwork requestwaiting outside JSQueuecallback readyUIbrowser staysinteractive
Slow work leaves the call stack, waits with the browser, then returns when JavaScript is free to continue.

Callbacks: the original async pattern

Before Promises, async code used callbacks β€” functions passed as arguments, to be called when work was done.

function loadUser(id, callback) {
  setTimeout(() => {
    const user = { id, name: 'Helen' };
    callback(user);
  }, 1000);
}

loadUser(1, (user) => {
  console.log(user.name); // runs after 1 second
});

This works. But callbacks have a well-known problem: nesting. When one async operation depends on another, which depends on another, you end up with deeply nested, hard-to-read code β€” often called callback hell.

loadUser(1, (user) => {
  loadPosts(user.id, (posts) => {
    loadComments(posts[0].id, (comments) => {
      // three levels deep and we're just getting started
    });
  });
});

Promises were introduced to solve this.


What is a Promise?

A Promise is an object that represents the eventual result of an asynchronous operation. It doesn't have the result yet β€” it's a promise that a result will arrive.

You can think of it like ordering a coffee. The barista hands you a number. You haven't got the coffee yet, but you have a promise of coffee. You can keep moving around, do other things, and when your number is called, you collect it.

A Promise has three possible states:

StateMeaning
PendingThe operation is in progress β€” no result yet
FulfilledThe operation completed successfully β€” a result is available
RejectedThe operation failed β€” an error is available

Once a Promise is fulfilled or rejected, it stays that way. It won't flip back to pending, and it won't change state again.


Promise state diagram showing pending leading to fulfilled or rejected.Pendingstill waitingFulfilledresult arrivesRejectederror arrivesresolve()reject()
A Promise starts pending, then settles once as either fulfilled or rejected.

Creating a Promise

You create a Promise with new Promise(), passing it an executor function with two parameters: resolve (call this when the work succeeds) and reject (call this when it fails).

const myPromise = new Promise((resolve, reject) => {
  const success = true;

  if (success) {
    resolve('Here is your result');
  } else {
    reject('Something went wrong');
  }
});

In practice you'll rarely create Promises from scratch β€” most of the time you'll be working with Promises returned by built-in APIs like fetch(). But understanding how they're built helps you understand how they behave.


Handling results with .then() and .catch()

Once you have a Promise, you handle its outcome using .then() for success and .catch() for errors.

myPromise
  .then((result) => {
    console.log(result); // 'Here is your result'
  })
  .catch((error) => {
    console.log(error); // only runs if rejected
  });

.then() and .catch() both return new Promises, which means you can chain them β€” each step receives the result of the previous one.

fetch('/api/user')
  .then((response) => response.json())   // step 1: parse the response
  .then((user) => {                       // step 2: use the parsed data
    console.log(user.name);
  })
  .catch((error) => {                     // catches any error in the chain
    console.error('Failed:', error);
  });

This flat chain is far more readable than nested callbacks β€” and it's the pattern fetch() is built on. You'll see it in the next tutorial.


A complete example

Here's a self-contained example that simulates loading a user after a short delay:

function getUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: 'Helen', role: 'admin' });
      } else {
        reject(new Error('Invalid user ID'));
      }
    }, 800);
  });
}

getUser(1)
  .then((user) => {
    console.log(`Welcome, ${user.name}`);
  })
  .catch((error) => {
    console.error(error.message);
  });

console.log('This runs immediately β€” before the user loads');

Notice that the final console.log runs before the user loads. That's async in action: JavaScript doesn't stop and wait, it moves on and handles the result when it's ready.


⏸️ Check Your Understanding

Before moving forward, can you answer these?

  1. 1. What does "single-threaded" mean for JavaScript?
  2. 2. What are the three states of a Promise?
  3. 3. What's the difference between `.then()` and `.catch()`?
  4. 4. Why can you chain `.then()` calls?
Check Your Answers
  1. JavaScript processes one piece of code at a time. It has one call stack and cannot run two things simultaneously.
  2. Pending (in progress), Fulfilled (succeeded, result available), Rejected (failed, error available).
  3. `.then()` runs when the Promise is fulfilled and receives the success result. `.catch()` runs when any Promise in the chain is rejected and receives the error.
  4. Because `.then()` itself returns a new Promise, passing its return value to the next step in the chain.

How confident are you with this concept?

πŸ˜• Still confused | πŸ€” Getting there | 😊 Got it! | πŸŽ‰ Could explain it to a friend!

Guided Practice

<!-- GuidedPractice component -->

Step 1 β€” Create the function

Write a function called getProduct that accepts an id parameter and returns a Promise.

function getProduct(id) {
  return new Promise((resolve, reject) => {
    // your code goes here
  });
}

Step 2 β€” Add the async logic

Inside the Promise, use setTimeout to simulate a 600ms delay. If id is a positive number, resolve with an object: { id, name: 'Espresso Machine', price: 299 }. If id is 0 or negative, reject with new Error('Product not found').

Hint

setTimeout(() => {
  if (id > 0) {
    resolve({ id, name: 'Espresso Machine', price: 299 });
  } else {
    reject(new Error('Product not found'));
  }
}, 600);

Step 3 β€” Call it with `.then()` and `.catch()`

Call getProduct(1) and chain .then() to log the product name and price. Add .catch() to log the error message.

Step 4 β€” Test the error case

Call getProduct(0). Confirm your .catch() handles it without crashing.

Step 5 β€” Add a log before the call

Add console.log('Requesting product...') before calling getProduct. Confirm it appears in the console before the product details β€” even though the delay is only 600ms.

πŸ’ͺ Independent Practice

<!-- IndependentPractice component -->

Your Task:

Build a small order status checker using Promises.

Requirements:
  • Write a function `checkOrderStatus(orderId)` that returns a Promise
  • Simulate a 1-second delay with `setTimeout`
  • If `orderId` is between 1000 and 9999 (a valid order number), resolve with an object: `{ orderId, status: 'dispatched', estimatedDelivery: '2 days' }`
  • If `orderId` is outside that range, reject with `new Error('Order not found')`
  • Call the function with a valid ID and log a readable message: e.g. *"Order 1042: dispatched β€” arrives in 2 days"*
  • Call the function with an invalid ID and handle the error gracefully

Success Criteria:

CriteriaYou've succeeded if...
`checkOrderStatus` returns a PromiseCompleted clearly and correctly in your solution.
Valid IDs resolve with the correct object shapeCompleted clearly and correctly in your solution.
Invalid IDs reject with a meaningful error messageCompleted clearly and correctly in your solution.
`.then()` produces a readable, formatted outputCompleted clearly and correctly in your solution.
`.catch()` handles the error without crashingCompleted clearly and correctly in your solution.
A log before the function call confirms async timingCompleted clearly and correctly in your solution.

What's next

Key Takeaways:

  • Explain what synchronous and asynchronous execution mean, and why the difference matters
  • Describe what a Promise is and what the three states of a Promise represent
  • Use `.then()` and `.catch()` to handle Promise results and errors
  • Read and interpret async code with confidence before writing it

Learning Objectives Review:

Look back at what you set out to learn. Can you now:

  • βœ… Explain what synchronous and asynchronous execution mean, and why the difference matters Check!
  • βœ… Describe what a Promise is and what the three states of a Promise represent Got it!
  • βœ… Use `.then()` and `.catch()` to handle Promise results and errors Can explain it!
  • βœ… Read and interpret async code with confidence before writing it Could teach this!

If you can confidently answer "yes" to most of these, you're ready to move on!

Think & Reflect:

πŸ’­ Pause and reflect

  • Which idea from this lesson now feels practical rather than abstract?
  • What would you build or test next to make this stick?

🎯 Looking Ahead:


You can now read and write Promise-based async code. The next step is putting that to practical use with fetch() β€” the browser's built-in tool for making network requests and loading real data from APIs.

In the next tutorial you'll make your first HTTP request, handle the two-step response pattern, and start working with real data from an external source.

β†’ Next: fetch() and the Request/Response Cycle

Recommended Next Steps

Continue Learning

Ready to move forward? Continue with the next tutorial in this series:

fetch() and the Request/Response Cycle

Related Topics

Explore these related tutorials to expand your knowledge:

Progress tracking is disabled. Enable it in to track your completed tutorials.