Introduction
What are Promises?
- Managing Asynchronous Operations: In JavaScript, many operations (like fetching data from a server, reading a file, or waiting for a timer) take time. Promises provide a structured way to handle the results of these asynchronous operations without getting tangled up in messy callbacks.
- A Proxy for Future Values: A Promise is an object that represents the eventual result of an asynchronous operation. It’s like a placeholder. Initially, the promise is in a “pending” state, but eventually, it will either:
- Fulfilled: The operation was successful, and a value is available.
- Rejected: The operation failed, and you get an error explaining why.
Key Methods
.then()
Used to handle the successful resolution of a Promise. It takes a callback function that receives the resolved value..catch()
Used to handle errors. It takes a callback function that receives the error object..finally()
Executes a callback function regardless of whether a promise is fulfilled or rejected. Often used for cleanup tasks.
import console from 'console';
import { setTimeout } from 'timers';
function delay(ms: number, shouldResolve: boolean): Promise<string> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldResolve) {
resolve('Promise resolved');
} else {
reject('Promise rejected');
}
}
, ms);
});
}
delay(1000, true).then(function(message) {
console.log(message); // This will log "Promise resolved"
}).catch(function(error) {
console.error(error); // This won't be called in this case
});
When you create a new Promise in TypeScript, you pass an executor function to the Promise constructor. This executor function takes two arguments: a resolve
function and a reject
function.
Here’s what each argument is for:
resolve
: This is a function that you call when the asynchronous operation completes successfully. You call this function with the result of the operation.reject
: This is a function that you call when the asynchronous operation fails. You call this function with the reason for the failure, which is typically an Error object.
(resolve, reject) => { ... }
- This is the executor function. It has two parameters:
resolve
: A function you call when the asynchronous operation has successfully completed. You must pass it the value that the Promise should resolve to.reject
: A function you call if the asynchronous operation fails. You pass it anError
object that explains the reason for the failure.
Code for JavaScript Promises
class MyPromise {
constructor(executor) {
this.state = 'pending';
this.value = undefined;
this.onFulfilledCallbacks = [];
this.onRejectedCallbacks = [];
const resolve = (value) => {
if (this.state === 'pending') {
if (value instanceof MyPromise) {
value.then(resolve, reject);
} else {
this.state = 'fulfilled';
this.value = value;
this.onFulfilledCallbacks.forEach(callback => callback(value));
}
}
};
const reject = (reason) => {
if (this.state === 'pending') {
this.state = 'rejected';
this.value = reason;
this.onRejectedCallbacks.forEach(callback => callback(reason));
}
};
try {
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
then(onFulfilled, onRejected) {
return new MyPromise((resolve, reject) => {
const handleFulfilled = (value) => {
try {
const result = onFulfilled(value);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
} catch (error) {
reject(error);
}
};
const handleRejected = (reason) => {
try {
const result = onRejected(reason);
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
reject(result);
}
} catch (error) {
reject(error);
}
};
if (this.state === 'fulfilled') {
handleFulfilled(this.value);
} else if (this.state === 'rejected') {
handleRejected(this.value);
} else {
this.onFulfilledCallbacks.push(handleFulfilled);
this.onRejectedCallbacks.push(handleRejected);
}
});
}
}
Design Choices
- The
MyPromise
class has aconstructor
that takes anexecutor
function. This function is immediately executed and is passed two functions:resolve
andreject
. These functions allow the executor to indicate the success or failure of the promise. - The
state
property is used to track the status of the promise. It can be ‘pending’, ‘fulfilled’, or ‘rejected’. - The
value
property is used to store the result of the promise. - The
onFulfilledCallbacks
andonRejectedCallbacks
arrays are used to store callback functions that will be executed when the promise is fulfilled or rejected.
- New Promise from
then
function:- The
then
method returns a new promise. This is a key feature of promises that enables chaining. The returned promise resolves or rejects based on the outcome of theonFulfilled
oronRejected
callbacks. If these callbacks return a value, the returned promise is resolved with that value. If they throw an error, the returned promise is rejected with that error.
- The
- Need for a Callback Array:
- The
onFulfilledCallbacks
andonRejectedCallbacks
arrays are needed because thethen
method can be called multiple times on the same promise. Each timethen
is called, a new callback is added to the respective array. When the promise is settled (either fulfilled or rejected), all the registered callbacks are called. This is known as “observation” of a promise. - These arrays are also useful when the
then
method is called after the promise has already settled. In this case, the callback is immediately called with the promise’s value or reason
- The
myPromise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('success');
}, 1000);
});
myPromise.then((value) => {
console.log(value);
});