From Zero to AI

Lesson 2.2: What is a Promise

Duration: 60 minutes

Learning Objectives

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

  • Explain what a Promise is and why it exists
  • Describe the three states of a Promise
  • Create Promises using the Promise constructor
  • Understand how Promises are resolved or rejected
  • Convert callback-based functions to Promises

What is a Promise?

A Promise is an object that represents the eventual completion (or failure) of an asynchronous operation and its resulting value.

Think of it like ordering food at a restaurant:

  1. You place an order (start an async operation)
  2. You receive a ticket number (a Promise)
  3. The kitchen prepares your food (operation in progress)
  4. Eventually, either:
    • Your food is ready (Promise fulfilled)
    • They ran out of ingredients (Promise rejected)
// A Promise represents a future value
const orderPromise: Promise<string> = placeOrder('burger');

// At this point, we don't have the burger yet
// But we have a Promise that we WILL get it (or an error)

Promise States

Every Promise is in one of three states:

1. Pending

The initial state. The operation has not completed yet.

const promise = new Promise<string>((resolve) => {
  // Still pending - resolve hasn't been called yet
  setTimeout(() => {
    resolve('Done!');
  }, 5000);
});

console.log(promise); // Promise { <pending> }

2. Fulfilled (Resolved)

The operation completed successfully. The Promise has a value.

const promise = new Promise<string>((resolve) => {
  resolve('Success!'); // Immediately fulfilled
});

console.log(promise); // Promise { 'Success!' }

3. Rejected

The operation failed. The Promise has a reason (error).

const promise = new Promise<string>((resolve, reject) => {
  reject(new Error('Something went wrong!')); // Immediately rejected
});

console.log(promise); // Promise { <rejected> Error: Something went wrong! }

State Diagram

                    ┌─────────────────┐
                    │                 │
    ┌───────────────│     PENDING     │───────────────┐
    │               │                 │               │
    │               └─────────────────┘               │
    │                                                 │
    │ resolve(value)                    reject(reason)│
    │                                                 │
    ▼                                                 ▼
┌─────────────────┐                     ┌─────────────────┐
│                 │                     │                 │
│   FULFILLED     │                     │    REJECTED     │
│   (has value)   │                     │   (has reason)  │
│                 │                     │                 │
└─────────────────┘                     └─────────────────┘

Important: Once a Promise is fulfilled or rejected, it is settled. It cannot change state again.


Creating Promises

The Promise constructor takes a function called the executor. The executor receives two functions:

  • resolve(value): Call this when the operation succeeds
  • reject(reason): Call this when the operation fails
const myPromise = new Promise<string>((resolve, reject) => {
  // Do some async work here
  const success = true;

  if (success) {
    resolve('Operation completed!');
  } else {
    reject(new Error('Operation failed!'));
  }
});

Example: Simulating an API Call

function fetchUserData(userId: number): Promise<{ id: number; name: string }> {
  return new Promise((resolve, reject) => {
    // Simulate network delay
    setTimeout(() => {
      if (userId <= 0) {
        reject(new Error('Invalid user ID'));
        return;
      }

      // Simulate successful response
      resolve({
        id: userId,
        name: 'John Doe',
      });
    }, 1000);
  });
}

// Usage
const userPromise = fetchUserData(1);
console.log(userPromise); // Promise { <pending> }

// After 1 second, the promise will be fulfilled with the user data

Example: Simulating File Reading

function readFileAsync(filename: string): Promise<string> {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (filename === '') {
        reject(new Error('Filename cannot be empty'));
        return;
      }

      if (filename === 'missing.txt') {
        reject(new Error('File not found'));
        return;
      }

      resolve(`Contents of ${filename}`);
    }, 500);
  });
}

// Returns a Promise that will be fulfilled or rejected
const filePromise = readFileAsync('data.txt');

The Executor Runs Immediately

The executor function runs synchronously when the Promise is created:

console.log('1: Before Promise');

const promise = new Promise<string>((resolve) => {
  console.log('2: Inside executor'); // Runs immediately!
  resolve('Done');
});

console.log('3: After Promise');

// Output:
// 1: Before Promise
// 2: Inside executor
// 3: After Promise

Only the resolve or reject callbacks trigger asynchronous behavior.


TypeScript and Promises

TypeScript provides generic types for Promises:

// Promise that resolves to a string
const stringPromise: Promise<string> = new Promise((resolve) => {
  resolve('Hello');
});

// Promise that resolves to a number
const numberPromise: Promise<number> = new Promise((resolve) => {
  resolve(42);
});

// Promise that resolves to an object
interface User {
  id: number;
  name: string;
  email: string;
}

const userPromise: Promise<User> = new Promise((resolve) => {
  resolve({
    id: 1,
    name: 'Alice',
    email: 'alice@example.com',
  });
});

// Promise that resolves to void (no meaningful value)
const voidPromise: Promise<void> = new Promise((resolve) => {
  console.log('Side effect!');
  resolve();
});

Type Inference

TypeScript usually infers the Promise type from the resolve call:

// TypeScript infers Promise<string>
const promise = new Promise((resolve) => {
  resolve('Hello');
});

// For complex types, be explicit
function getUser(): Promise<{ id: number; name: string }> {
  return new Promise((resolve) => {
    resolve({ id: 1, name: 'Alice' });
  });
}

Converting Callbacks to Promises

A common task is wrapping callback-based functions in Promises. This is called promisification.

Before (Callback-based)

function getUserCallback(
  id: number,
  callback: (error: Error | null, user: { name: string } | null) => void
): void {
  setTimeout(() => {
    if (id <= 0) {
      callback(new Error('Invalid ID'), null);
    } else {
      callback(null, { name: 'Alice' });
    }
  }, 100);
}

After (Promise-based)

function getUserPromise(id: number): Promise<{ name: string }> {
  return new Promise((resolve, reject) => {
    getUserCallback(id, (error, user) => {
      if (error) {
        reject(error);
      } else {
        resolve(user!);
      }
    });
  });
}

Generic Promisify Function

You can create a utility to promisify any callback-based function:

function promisify<T>(
  fn: (callback: (error: Error | null, result: T | null) => void) => void
): () => Promise<T> {
  return () => {
    return new Promise((resolve, reject) => {
      fn((error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result!);
        }
      });
    });
  };
}

// Usage
function legacyGetData(callback: (error: Error | null, data: string | null) => void): void {
  setTimeout(() => callback(null, 'Legacy data'), 100);
}

const getDataPromise = promisify(legacyGetData);
const dataPromise = getDataPromise(); // Returns Promise<string>

Promise.resolve and Promise.reject

Sometimes you need to create already-settled Promises:

Promise.resolve

Creates a Promise that is immediately fulfilled:

// Immediately fulfilled with "Hello"
const resolved = Promise.resolve('Hello');

// Useful for returning synchronous values as Promises
function getData(cached: boolean): Promise<string> {
  if (cached) {
    return Promise.resolve('Cached data'); // No async needed
  }
  return fetchFromServer(); // Returns Promise<string>
}

Promise.reject

Creates a Promise that is immediately rejected:

// Immediately rejected with an error
const rejected = Promise.reject(new Error('Something went wrong'));

// Useful for early validation
function validateAndFetch(id: number): Promise<{ name: string }> {
  if (id <= 0) {
    return Promise.reject(new Error('ID must be positive'));
  }
  return fetchUser(id);
}

Common Mistakes

Mistake 1: Forgetting to Return the Promise

// Wrong - returns undefined!
function getUser(id: number) {
  new Promise((resolve) => {
    resolve({ id, name: 'Alice' });
  });
}

// Correct - returns the Promise
function getUser(id: number): Promise<{ id: number; name: string }> {
  return new Promise((resolve) => {
    resolve({ id, name: 'Alice' });
  });
}

Mistake 2: Calling Both resolve and reject

// Wrong - only the first call matters, but this is confusing
const promise = new Promise<string>((resolve, reject) => {
  resolve("Success");
  reject(new Error("Error")); // This is ignored!
});

// Correct - use early return
const promise = new Promise<string>((resolve, reject) => {
  if (someCondition) {
    reject(new Error("Error"));
    return; // Stop execution
  }
  resolve("Success");
});

Mistake 3: Not Handling Errors

// Dangerous - errors are silently ignored
const promise = new Promise<string>((resolve, reject) => {
  reject(new Error('Oops!'));
});
// No .catch() - UnhandledPromiseRejection!

// Always handle rejections (we'll cover .catch() in the next lesson)

Mistake 4: Resolve with Another Promise

// This works but can be confusing
const inner = Promise.resolve(42);
const outer = Promise.resolve(inner);

// outer is Promise<number>, not Promise<Promise<number>>
// JavaScript automatically "unwraps" nested Promises

Practical Example: Timer Promise

Let us create a reusable delay function:

function delay(ms: number): Promise<void> {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

// Usage
console.log('Starting...');
delay(2000).then(() => {
  console.log('2 seconds later!');
});

Timer with Value

function delayedValue<T>(ms: number, value: T): Promise<T> {
  return new Promise((resolve) => {
    setTimeout(() => resolve(value), ms);
  });
}

// Usage
delayedValue(1000, 'Hello').then((message) => {
  console.log(message); // "Hello" after 1 second
});

Timeout Promise

function timeout(ms: number): Promise<never> {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`Operation timed out after ${ms}ms`));
    }, ms);
  });
}

// We'll use this with Promise.race() in Lesson 2.4

Exercises

Exercise 1: Create a Simple Promise

Create a function randomNumber that returns a Promise. The Promise should:

  • Wait 500ms
  • Resolve with a random number between 1 and 100
function randomNumber(): Promise<number> {}

// Test it
randomNumber().then((num) => console.log('Random:', num));
Solution
function randomNumber(): Promise<number> {
  return new Promise((resolve) => {
    setTimeout(() => {
      const num = Math.floor(Math.random() * 100) + 1;
      resolve(num);
    }, 500);
  });
}

randomNumber().then((num) => console.log('Random:', num));

Exercise 2: Promise with Validation

Create a function divide that returns a Promise. It should:

  • Take two numbers as parameters
  • Reject if the second number is 0
  • Resolve with the result otherwise
function divide(a: number, b: number): Promise<number> {}

// Test it
divide(10, 2).then((result) => console.log('Result:', result));
divide(10, 0).catch((error) => console.log('Error:', error.message));
Solution
function divide(a: number, b: number): Promise<number> {
  return new Promise((resolve, reject) => {
    if (b === 0) {
      reject(new Error('Cannot divide by zero'));
      return;
    }
    resolve(a / b);
  });
}

divide(10, 2).then((result) => console.log('Result:', result)); // Result: 5
divide(10, 0).catch((error) => console.log('Error:', error.message)); // Error: Cannot divide by zero

Exercise 3: Convert Callback to Promise

Convert this callback function to return a Promise:

// Original callback version
function checkAge(
  age: number,
  callback: (error: Error | null, allowed: boolean | null) => void
): void {
  setTimeout(() => {
    if (age < 0) {
      callback(new Error('Age cannot be negative'), null);
    } else if (age >= 18) {
      callback(null, true);
    } else {
      callback(null, false);
    }
  }, 100);
}

// Convert to:
function checkAgePromise(age: number): Promise<boolean> {}
Solution
function checkAgePromise(age: number): Promise<boolean> {
  return new Promise((resolve, reject) => {
    checkAge(age, (error, allowed) => {
      if (error) {
        reject(error);
      } else {
        resolve(allowed!);
      }
    });
  });
}

// Test it
checkAgePromise(25).then((allowed) => console.log('Allowed:', allowed)); // true
checkAgePromise(15).then((allowed) => console.log('Allowed:', allowed)); // false
checkAgePromise(-5).catch((error) => console.log('Error:', error.message)); // Age cannot be negative

Exercise 4: Identify Promise States

What state is each Promise in immediately after creation?

// 1
const p1 = new Promise((resolve) => {
  resolve('Done');
});

// 2
const p2 = new Promise((resolve) => {
  setTimeout(() => resolve('Done'), 1000);
});

// 3
const p3 = Promise.reject(new Error('Failed'));

// 4
const p4 = new Promise((resolve, reject) => {
  const random = Math.random();
  if (random > 0.5) resolve('High');
  else reject(new Error('Low'));
});
Solution
  1. Fulfilled - resolve is called synchronously
  2. Pending - resolve will be called after 1 second
  3. Rejected - Promise.reject creates an already-rejected Promise
  4. Either Fulfilled or Rejected - depends on the random number (determined at creation time)

Key Takeaways

  1. A Promise represents a future value (success or failure)
  2. Promises have three states: pending, fulfilled, rejected
  3. Once settled (fulfilled or rejected), a Promise cannot change state
  4. Use the Promise constructor with resolve and reject functions
  5. The executor function runs synchronously
  6. Use Promise.resolve() and Promise.reject() for immediate values
  7. Promisification converts callback-based functions to Promise-based
  8. TypeScript uses Promise<T> to type the resolved value

Resources

Resource Type Description
MDN: Promise Documentation Complete Promise reference
JavaScript.info: Promise Tutorial Interactive Promise basics
TypeScript Promise Types Documentation Promise typing in TypeScript

Next Lesson

Now that you can create Promises, let us learn how to consume them using .then(), .catch(), and .finally().

Continue to Lesson 2.3: then, catch, finally