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:
- You place an order (start an async operation)
- You receive a ticket number (a Promise)
- The kitchen prepares your food (operation in progress)
- 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 succeedsreject(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
- Fulfilled -
resolveis called synchronously - Pending -
resolvewill be called after 1 second - Rejected -
Promise.rejectcreates an already-rejected Promise - Either Fulfilled or Rejected - depends on the random number (determined at creation time)
Key Takeaways
- A Promise represents a future value (success or failure)
- Promises have three states: pending, fulfilled, rejected
- Once settled (fulfilled or rejected), a Promise cannot change state
- Use the Promise constructor with
resolveandrejectfunctions - The executor function runs synchronously
- Use
Promise.resolve()andPromise.reject()for immediate values - Promisification converts callback-based functions to Promise-based
- 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().