JavaScript • Asynchronous JavaScript

Creating & Consuming Promises — Create and consume Promises (.then/.catch)

Master JavaScript's built-in system for handling tasks that take time, using Promises with .then() and .catch().

JavaScript emblem with code brackets, braces, and a code file.
Inside this lesson 10 sections 5 questions 7 figures

SECTION 1

Historical Context & Motivation

In the early days of JavaScript, the language was designed primarily to add small interactive features to web pages — things like form validation or simple animations. As the web grew more complex, developers started building applications that needed to fetch data from servers, read files, or wait for user input. These operations take time, and the browser can't just freeze and wait for them to finish. The original solution was callback functions — you would pass a function into another function, and that inner function would run once the slow task completed.

Callbacks worked, but they had a serious problem. When you needed to chain multiple asynchronous operations together — like fetching a user, then fetching their posts, then fetching comments on those posts — the code became deeply nested and nearly impossible to read. Developers called this nightmare callback hell (sometimes called the "pyramid of doom"). The JavaScript community needed a cleaner pattern, and that need gave birth to Promises.

1995 • JavaScript is Born

Brendan Eich creates JavaScript in just ten days at Netscape. Asynchronous operations are handled through basic callback functions, which work but can become messy.

2011 • jQuery Deferred Objects

The jQuery library popularizes a Promise-like pattern called Deferred objects, showing developers the power of chainable asynchronous code.

2012 • Promises/A+ Specification

A community-driven specification standardizes how Promises should behave, creating a consistent contract that all implementations can follow.

2015 • ES6 — Native Promises

ECMAScript 2015 (ES6) adds Promises as a built-in language feature. Every modern browser and Node.js now supports them natively.

2017 • async/await Syntax

ES2017 introduces async/await — a cleaner syntax built on top of Promises, making asynchronous code look almost synchronous.

The central question that Promises answer is straightforward: how can we write clean, readable code that handles operations which haven't finished yet? Understanding how to create and consume Promises is essential for every JavaScript developer, because almost everything on the modern web — fetching API data, loading images, reading databases — relies on this pattern.

SECTION 2

Core Principles & Definitions

A Promise is a JavaScript object that represents the eventual result of an asynchronous operation. When you create a Promise, you are essentially saying: "I don't have the answer right now, but I promise to give you one later — either a success value or an error." This object can be in one of three states, and understanding these states is the key to working with Promises effectively.

1

Pending

The initial state of every Promise. The asynchronous operation is still running — it hasn't succeeded or failed yet. Think of it as a package that's been shipped but hasn't arrived.
2

Fulfilled (Resolved)

The operation completed successfully. The Promise now holds a result value that you can access through .then().
3

Rejected

The operation failed. The Promise now holds an error reason that you can handle through .catch().
4

Settled

A general term meaning the Promise is no longer pending — it has either been fulfilled or rejected. Once settled, a Promise's state never changes again.

Two methods form the backbone of consuming a Promise. The .then() method registers a callback that runs when the Promise is fulfilled, receiving the success value as its argument. The .catch() method registers a callback that runs when the Promise is rejected, receiving the error as its argument. Because both .then() and .catch() return new Promises themselves, you can chain them together to build clean, sequential workflows.

✦

KEY TAKEAWAY

Think of a Promise like ordering food at a restaurant. When you place your order, the waiter gives you a ticket number (the pending Promise). You don't have the food yet, but you have a guarantee that either your meal will arrive (fulfilled) or the kitchen will tell you they're out of that dish (rejected). You don't need to stand at the counter and stare — you can go sit down and do other things while you wait. That's exactly how JavaScript handles asynchronous work.

SECTION 3

Visual Explanation — The Promise Lifecycle

Promise Lifecyclenew Promise()executor runsPENDINGwaiting…FULFILLEDresolve(value)has resultREJECTEDreject(error)has reasonresolve()reject().then(callback)runs on fulfillment.catch(callback)runs on rejection.finally(callback)runs either wayConsumer MethodsEach .then() or .catch() returns a new Promise, enabling chaining →A settled Promise never changes state again.
The diagram shows the three states of a Promise — pending, fulfilled, and rejected — along with the consumer methods .then(), .catch(), and .finally() that let you react to each outcome.

When you create a new Promise with new Promise(), you pass in an executor function that receives two arguments: resolve and reject. These are functions provided by JavaScript itself. Calling resolve(value) moves the Promise from pending to fulfilled, and calling reject(error) moves it from pending to rejected. Notice in the diagram that the arrows only go in one direction — once a Promise settles, its fate is sealed.

The bottom row shows the consumer methods. When the Promise is fulfilled, the callback inside .then() fires and receives the resolved value. When it's rejected, the callback inside .catch() fires and receives the error. The arrows between the consumer boxes represent chaining — each method returns a brand-new Promise, so you can keep adding .then() calls one after another.

SECTION 4

How It Works — Creating a Promise

The Promise Constructor

To create a Promise, you use the new Promise() constructor. The constructor takes a single argument: the executor function. This function runs immediately — not later — and receives two parameters that JavaScript provides for you.

PROMISE CONSTRUCTOR PATTERN

constmyPromise=newPromise((resolve,reject)=>...);const myPromise = new Promise((resolve, reject) => { ... });

resolve(value) — call this when the operation succeeds, passing the result. reject(error) — call this when the operation fails, passing an error or message.

Consuming with .then() and .catch()

Once you have a Promise, you consume it by attaching handlers. The .then() method takes up to two arguments: an onFulfilled callback and an optional onRejected callback. In practice, most developers prefer to use .catch() for error handling because it catches errors from the entire chain, not just the preceding Promise.

CONSUMPTION PATTERN

myPromise.then(value=>...).catch(error=>...);myPromise.then(value => { ... }).catch(error => { ... });

value — the result passed to resolve(). error — the reason passed to reject() or any thrown error in the chain.

Chaining Promises

One of the most powerful features of Promises is chaining. Because .then() always returns a new Promise, you can line up multiple asynchronous steps in a flat, readable sequence. Each .then() receives the return value of the previous .then() as its input. If any step in the chain throws an error or returns a rejected Promise, control jumps directly to the nearest .catch(), skipping any intermediate .then() handlers.

CHAINING PATTERN

fetchUser(id).then(user=>fetchPosts(user)).then(posts=>displayPosts(posts)).catch(err=>showError(err));fetchUser(id).then(user => fetchPosts(user)).then(posts => displayPosts(posts)).catch(err => showError(err));

Each .then() feeds its return value into the next one. A single .catch() at the end handles errors from any step in the chain.

SECTION 5

Anatomy of a Promise Chain

Let's look at a realistic example to understand exactly how data flows through a Promise chain. Imagine you're building a web app that loads a student's profile, then loads their grades, and finally displays everything on screen. Each of these steps is asynchronous because it involves fetching data from a server.

Promise Chain — Data FlowSTEP 1getStudent(id)Returns a PromiseSTEP 2 — .then()getGrades(student)Receives student objSTEP 3 — .then()display(grades)Renders to page.catch(error => showError(error))Catches errors from ANY step aboveerrorerrorerrorCode Example:getStudent("s123") .then(student => getGrades(student)) .then(grades => display(grades)) .catch(error => showError(error));
This diagram traces how data moves through a three-step Promise chain. The yellow step starts the chain, each cyan and green step receives the previous result, and the red dashed lines show how any error skips straight to .catch().

Notice the key advantage over callback hell: the code reads top-to-bottom in a flat sequence rather than nesting deeper and deeper. Each .then() block does one thing, and you only need a single .catch() at the end. If getGrades fails (maybe the server is down), the error bypasses the display step entirely and lands in the .catch() handler. This pattern is sometimes called the error propagation behavior of Promises.

⚠

Common Mistake

A frequent beginner error is forgetting to return a value inside a .then() callback. If you don't return anything, the next .then() receives undefined instead of the data you expected. Always return the value or Promise you want passed along.

SECTION 6

Worked Example — Simulating a Data Fetch

Let's walk through a complete example step by step. We'll create a Promise that simulates fetching a student's name from a database (using setTimeout to fake the delay), then consume it with .then() and .catch().

Fetching a Student Name with a Promise

  1. 1

    Step 1 — Create the Promise

    We use new Promise() and pass in an executor function. Inside, we use setTimeout to simulate a 2-second network delay. If we find a student, we call resolve(); otherwise, we call reject().
    const fetchStudent = new Promise((resolve, reject) => { setTimeout(() => { const found = true; if (found) { resolve("Alice Johnson"); } else { reject("Student not found"); } }, 2000); });
  2. 2

    Step 2 — Attach the .then() handler

    We call .then() on our Promise and pass a callback function. This callback will only run if the Promise resolves successfully. The parameter name receives whatever value was passed to resolve().
    fetchStudent.then(name => { console.log("Found: " + name); }); → Logs: "Found: Alice Johnson"
  3. 3

    Step 3 — Attach the .catch() handler

    We chain a .catch() to handle the rejection case. If found were false, the reject() call would trigger this handler instead.
    fetchStudent.then(name => { console.log("Found: " + name); }).catch(err => { console.error("Error: " + err); });
  4. 4

    Step 4 — Add chaining to transform the result

    Suppose we want to convert the name to uppercase before logging it. We add another .then() in between. The first .then() returns the uppercase string, and the second .then() receives it.
    fetchStudent.then(name => { return name.toUpperCase(); }).then(upper => { console.log(upper); }).catch(err => { console.error(err); }); → Logs: "ALICE JOHNSON"
  5. 5

    Step 5 — Verify the execution order

    Even though we attach .then() immediately, the callback doesn't run until the Promise resolves. Any synchronous code after the chain runs first. This demonstrates JavaScript's event loop behavior.
    console.log("Before"); fetchStudent.then(name => console.log(name)); console.log("After"); → Output order: "Before" → "After" → "Alice Johnson"

SECTION 7

Callbacks vs. Promises — Strengths & Limitations

Understanding why Promises are an improvement over plain callbacks is important, but it's equally important to know their limitations. The table below compares the two approaches across several key dimensions that matter when building real applications.

FeatureCallbacksPromises
ReadabilityDeeply nested (callback hell); hard to follow for more than 2–3 stepsFlat chaining with .then(); reads top to bottom
Error handlingMust check for errors in every single callback; easy to miss oneSingle .catch() at the end catches errors from the entire chain
ComposabilityDifficult to combine or run operations in parallelPromise.all(), Promise.race() make composition easy
Inversion of controlYou hand your callback to another function — you trust it will call it correctlyYou retain control; you decide what to do with the result
Learning curveSimpler concept — just pass a functionRequires understanding states, chaining, and return values
LimitationCannot easily represent a value that doesn't exist yetCannot cancel a Promise once created (no built-in cancellation)

Comparing callbacks and Promises across key dimensions

✦

KEY TAKEAWAY

Promises don't add new superpowers to JavaScript — they reorganize existing capabilities into a cleaner mental model. Think of callbacks as writing step-by-step directions buried inside other directions ("turn left, then inside that paragraph find where to turn right..."). Promises are like a numbered list: step 1, step 2, step 3 — flat and scannable. The logic is the same, but the structure makes it far easier to understand and debug.

SECTION 8

Connection to async/await & Advanced Patterns

Once you're comfortable with .then() and .catch(), you're ready to learn async/await — a syntax introduced in ES2017 that makes Promise-based code look and feel like regular synchronous code. Under the hood, async/await still uses Promises. The await keyword is essentially syntactic sugar for .then(), and try/catch blocks replace .catch(). Understanding Promises deeply is therefore a prerequisite for using async/await effectively.

Feature.then()/.catch()async/await
Syntax styleMethod chaining on Promise objectsKeywords in regular-looking functions
Error handling.catch() methodtry { } catch { } block
Underlying mechanismDirect Promise API usagePromises under the hood — await unwraps a Promise
Best forSimple chains; when you need to branch conditionallyComplex sequential logic; when readability is top priority

Comparing .then/.catch with async/await

Beyond async/await, the Promise API includes powerful static methods for managing multiple Promises at once. Promise.all() takes an array of Promises and resolves only when all of them succeed — perfect for loading several resources at the same time. Promise.race() resolves as soon as any single Promise in the array settles, which is useful for implementing timeouts. These patterns build on the exact same .then() and .catch() foundation you've learned in this lesson.

ℹ

Looking Ahead

Every modern JavaScript API — from fetch() for HTTP requests to the File System API in Node.js — returns Promises. The skills you've built here will be used in nearly every project you write going forward.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

A Promise is created and both resolve("hello") and reject("oops") are called inside the executor (resolve is called first). What state does the Promise end up in, and what value does .then() receive? Explain why.
Show answer
The Promise ends up in the fulfilled state with the value "hello". Once a Promise settles (either resolved or rejected), its state is locked and cannot change. The subsequent call to reject("oops") is simply ignored because the Promise has already been fulfilled. This is the "settled once" rule — a fundamental principle of Promise behavior. The .then() handler receives "hello" and the .catch() handler never runs.

Question 2: BASIC CALCULATION

Write a Promise called doubleIt that takes a number 5, waits 1 second (using setTimeout), then resolves with the number doubled. Consume the Promise with .then() to log the result.
Show answer
Here is the solution: const doubleIt = new Promise((resolve, reject) => { setTimeout(() => { resolve(5 * 2); }, 1000); }); doubleIt.then(result => console.log(result)); After 1 second, this logs 10 to the console. The executor runs immediately, setting up the timeout. When the timeout fires, resolve(10) is called, which triggers the .then() callback. The result parameter receives the value 10 that was passed to resolve.

Question 3: INTERMEDIATE

Given the following code, predict the exact output and explain the order: console.log("A"); const p = new Promise((resolve) => { console.log("B"); resolve("C"); }); p.then(val => console.log(val)); console.log("D");
Show answer
The output order is: A → B → D → C. First, "A" logs synchronously. Then the Promise executor runs immediately (it's synchronous), so "B" logs next. The resolve("C") call schedules the .then() callback as a microtask, but it doesn't run yet. The synchronous console.log("D") executes next. Finally, once all synchronous code is done, the JavaScript event loop processes the microtask queue, and "C" is logged.

Question 4: APPLIED

You're building a weather app. Write a function getTemperature(city) that returns a Promise. If the city is "New York", resolve with 72. If the city is "London", resolve with 58. For any other city, reject with "City not found". Then chain a .then() that converts the temperature to Celsius (subtract 32, multiply by 5/9, round to 1 decimal place) and a .catch() for the error.
Show answer
function getTemperature(city) { return new Promise((resolve, reject) => { if (city === "New York") resolve(72); else if (city === "London") resolve(58); else reject("City not found"); }); } getTemperature("New York").then(f => { const celsius = Math.round(((f - 32) * 5 / 9) * 10) / 10; console.log(celsius + "°C"); }).catch(err => console.error(err)); For "New York", this logs 22.2°C. The function wraps conditional logic inside a Promise executor. The .then() receives the Fahrenheit value and converts it. Calling getTemperature("Tokyo") would skip .then() and trigger .catch() with "City not found".

Question 5: CRITICAL THINKING

Consider this chain: Promise.resolve(1).then(x => x + 1).then(x => { throw new Error("boom"); }).then(x => x + 1).catch(err => { console.log(err.message); return 99; }).then(x => console.log(x)); What is the complete console output? Explain why the final .then() still runs after the .catch().
Show answer
The output is "boom" followed by 99. Here's the chain in detail: Promise.resolve(1) creates a fulfilled Promise with value 1. The first .then() returns 2. The second .then() throws an error, so the chain is now rejected. The third .then(x => x + 1) is skipped because the chain is in a rejected state. The .catch() handles the error and logs "boom". Critically, .catch() returns a new fulfilled Promise (with the value 99), which means the chain recovers from the error. The final .then() receives 99 and logs it. This demonstrates that .catch() is not just an endpoint — it can heal the chain and pass values forward.

SUMMARY

Lesson Summary

Summary

A Promise is a JavaScript object representing the eventual result of an asynchronous operation. Every Promise begins in the pending state and settles exactly once — becoming either fulfilled (via resolve()) or rejected (via reject()). You create a Promise with the new Promise() constructor, passing an executor function that contains your asynchronous logic.

To consume a Promise, attach .then() for success handling and .catch() for error handling. Because both methods return new Promises, you can build flat, readable chains instead of deeply nested callbacks. Errors propagate down the chain until caught by a .catch(), and a .catch() can even recover the chain by returning a value. This pattern is the foundation for async/await and virtually all modern JavaScript asynchronous programming.

Varsity Tutors • JavaScript • Creating & Consuming Promises — Create and consume Promises (.then/.catch)