Promises: When Your Code Swears to Deliver

Aman Singh
Newton School
Published in
9 min readDec 28, 2022

A Lighthearted Look at the Inner Workings of JavaScript’s Go-To Async Tool

Prerequisite — Observer design Pattern, using classes in JS

Observer Design Pattern

The observer design pattern is a way for one piece of code to keep track of events happening elsewhere. In JavaScript, we use it all the time with event listeners and handlers. Here’s an example: say you have a button on a webpage, and you want to do something special every time someone clicks it. You can set up an event listener to listen for the click event, and then your code gets notified every time the button is clicked. It’s like a subscription service for your code. Your code can subscribe to a host (like the button), and then it gets notified whenever something happens (like a click). But if you ever get tired of being notified, you can always unsubscribe and take a break. The observer design pattern is a great way to add some interactivity to your JavaScript code, and it helps to keep your code organised and easy to understand.

Creating observer design pattern in JavaScript

To create the observer design pattern, we need to have two types of participants.

Host

It will maintain the list of observers. Provides options to subscribe and unsubscribe to the observers Notifies the observer when state changes.

Observer

Has a function that gets called/invoked every time a state changes. Keeping these two things in mind, we can create the Observer design pattern in JavaScript.

Example Code

const student = {
name: 'Ram Lal',
skills: [],
observers: [],
learn(skill) {
this.skills.push(skill);
this.notify(skill);
},
subscribe(fn) {
this.observers.push(fn);
},
unsubscribe(fn) {
this.observers = this.observers.filter(observer => observer !== fn);
},
notify(skill) {
this.observers.forEach(observer => observer(this.name, skill));
},
};

const teacher1 = (name, skill) => console.log(`Teacher 1 is impressed that ${name} learned ${skill}`);
const teacher2 = (name, skill) => console.log(`Teacher 2 is amazed that ${name} mastered ${skill}`);

student.subscribe(teacher1);
student.subscribe(teacher2);

student.learn('JavaScript');
student.learn('React');

student.unsubscribe(teacher2);

student.learn('DSA');

Output

Teacher 1 is impressed that Ram Lal learned JavaScript
Teacher 2 is amazed that Ram Lal mastered JavaScript
Teacher 1 is impressed that Ram Lal learned React
Teacher 2 is amazed that Ram Lal mastered React
Teacher 1 is impressed that Ram Lal learned DSA

Explanation

In this example, the student object represents a student who is learning programming. The teacher1 and teacher2 functions represent teachers who are interested in the student’s progress. The subscribe() method adds a teacher to a list of people who are interested in the student’s progress. The unsubscribe() method removes a teacher from this list. The learn() method adds a new skill to the student’s list of skills and tells all of the teachers in the list about the new skill. The notify() method is used to tell all of the teachers about the new skill. The teacher1 and teacher2 functions are called with the student’s name and the new skill as arguments.

The above code can be written in same way using javascript class which are nothing but template for creating objects.

class Student {
constructor(name) {
this.name = name;
this.skills = [];
this.observers = [];
}

learn(skill) {
this.skills.push(skill);
this.notify(skill);
}

subscribe(fn) {
this.observers.push(fn);
}

unsubscribe(fn) {
this.observers = this.observers.filter(observer => observer !== fn);
}

notify(skill) {
this.observers.forEach(observer => observer(this.name, skill));
}
}

const student = new Student('Ram Lal');

const teacher1 = (name, skill) => console.log(`Teacher 1 is impressed that ${name} learned ${skill}`);
const teacher2 = (name, skill) => console.log(`Teacher 2 is amazed that ${name} mastered ${skill}`);

student.subscribe(teacher1);
student.subscribe(teacher2);

student.learn('JavaScript');
student.learn('HTML');

student.unsubscribe(teacher2);

student.learn('CSS');

Understanding Promises in JS

History of Promise in Javascript

JavaScript is a single-threaded language, which means that it can only execute one task at a time. This can make it difficult to perform long-running tasks, such as making network requests or reading large files, without blocking the main thread and preventing the browser from responding to user input.

To solve this problem, JavaScript introduced the concept of promises. A promise is an object that represents the result of an asynchronous operation. It allows you to write asynchronous code that looks and behaves like synchronous code, using the familiar syntax of the JavaScript language.

Promises were introduced in JavaScript in the mid-2010s, as a way to simplify the process of working with asynchronous code. Prior to promises, developers had to use callback functions to handle asynchronous operations, which could lead to callback hell — a situation where callback functions are nested inside each other, making the code difficult to read and understand, or inversion of control.

How callback hell looks like in code

Lets see how promises work with code examples

Example 1

console.log('start');

//A new promise is created using Promise Constructor
const promise = new Promise(
(resolve, reject) => {
console.log(1)
}
)
console.log('end');

Analysis

In above code, a new promise object is created using the Promise constructor (remember how we created the object of student class in Observer design pattern) and an computation function. The computation function takes two arguments: resolve and reject. These are functions that are used to either fulfill or reject the promise, respectively.

Computation function passed to Promise Constructor

In above code, computation function contains a single line of code: console.log(1). This line of code will be executed immediately when the promise is created.

It is important to note that the promise object itself does not have a value until it is either resolved or rejected. The value of the promise is passed to the resolve or reject function when the promise is fulfilled or rejected, respectively.

In the above code,

  • Synchronized code blocks are always executed sequentially from top to bottom. (remember JS is single threaded)
  • When we call new Promise(callback), the callback function will be executed immediately.

Result

So this code is to sequentially output start, 1, end.

Example 2

console.log('start');

const promise1 = new Promise((resolve, reject) => {
console.log(1)
resolve(2)
})

promise1.then(res => {
console.log(res)
})

console.log('end');

Analysis

In this code snippet, a piece of asynchronous code appears. That is, the callback function in .then().

Remember, the JavaScript engine always executes synchronous code first, then asynchronous code.

When encountering this problem, we only need to distinguish between synchronous code and asynchronous code.

Result

So the output is start , 1 , end and 2 .

Under the hood, promises use a state machine to keep track of the progress of an asynchronous operation. When a promise is first created, it’s in the “pending” state. As the operation represented by the promise progresses, the promise’s state can change to either “fulfilled” or “rejected”. If the operation completes successfully, the promise’s state becomes “fulfilled” and the result of the operation is stored in the promise. If the operation fails, the promise’s state becomes “rejected” and the error that caused the failure is stored in the promise.

The then() method is just one of the many methods that you can use to work with promises. For example, you can use the catch() method to specify a callback function that will be executed if the promise is rejected.

Lets work through each layer of abstraction of Promise in JS, and try to implement our own version.

const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve("hello");
}, 1000);
});
promise.then((value) => {
console.log(value);
});

In the above code, we have a computation function, which contains asynchronous computation. The computation function itself runs synchronously (check the examples above), only resolve and reject function runs asynchronously.

We add the observer to our computation function using then() and catch() method, which accepts our callback function or give error incase the execution fails.

Code

// enum of states
const states = {
PENDING: 0,
FULFILLED: 1,
REJECTED: 2,
};
class MyPromise {
// initialize the promise
constructor(callback) {
this.state = states.PENDING;
this.value = undefined;
this.handlers = [];
try {
callback(this._resolve, this._reject);
} catch (error) {
this._reject(error);
}
}
// helper function for resolve
_resolve = (value) => {
this._handleUpdate(states.FULFILLED, value);
};
// helper function for reject
_reject = (value) => {
this._handleUpdate(states.REJECTED, value);
};
// handle the state change
_handleUpdate = (state, value) => {
if (state === states.PENDING) {
return;
}
setTimeout(() => {
if (value instanceof MyPromise) {
value.then(this._resolve, this._reject);
}
this.state = state;
this.value = value;
this._executeHandlers();
}, 0);
};
// execute all the handlers
// depending on the current state
_executeHandlers = () => {
if (this.state === states.PENDING) {
return;
}
this.handlers.forEach((handler) => {
if (this.state === states.FULFILLED) {
return handler.onSuccess(this.value);
}
return handler.onFailure(this.value);
});
this.handlers = [];
};
// add handlers
// execute all if any new handler is added
_addHandler = (handler) => {
this.handlers.push(handler);
this._executeHandlers();
};
// then handler
// creates a new promise
// assigned the handler
then = (onSuccess, onFailure) => {
// invoke the constructor
// and new handler
return new MyPromise((resolve, reject) => {
this._addHandler({
onSuccess: (value) => {
if (!onSuccess) {
return resolve(value);
}
try {
return resolve(onSuccess(value));
} catch (error) {
reject(error);
}
},
onFailure: (value) => {
if (!onFailure) {
return reject(value);
}
try {
return reject(onFailure(value));
} catch (error) {
return reject(error);
}
},
});
});
};
// add catch handler
catch = (onFailure) => {
return this.then(null, onFailure);
};
// add the finally handler
finally = (callback) => {
// create a new constructor
// listen the then and catch method
// finally perform the action
return new MyPromise((resolve, reject) => {
let wasResolved;
let value;
this.then((val) => {
value = val;
wasResolved = true;
return callback();
}).catch((err) => {
value = err;
wasResolved = false;
return callback();
});
if (wasResolved) {
resolve(value);
} else {
reject(value);
}
});
};
}

const promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve("hello");
}, 1000);
});
promise.then((value) => {
console.log(value);
});

Output

hello

The above code defines a custom Promise class in JavaScript called MyPromise. This class has a constructor that takes a callback function as an argument. The callback function should accept two arguments: a function for resolving the promise, and a function for rejecting the promise.

Inside the constructor, the state property is initialized to PENDING, the value property is initialized to undefined, and the handlers property is initialized as an empty array. The try block calls the callback function and passes in the _resolve and _reject private methods as arguments. If an error occurs inside the callback function, it is caught by the catch block and passed to the _reject function.

The _resolve and _reject functions are helper functions that are used to change the state of the promise and store the result of the asynchronous operation. The _handleUpdate function is a helper function that is called by _resolve and _reject to change the state of the promise and store the result. If the new state is not PENDING, the function uses setTimeout() to execute the _executeHandlers function asynchronously.

The _executeHandlers function iterates through the handlers array and executes the appropriate callback function based on the current state of the promise. The _addHandler function adds a new handler to the handlers array and executes all the handlers if the state of the promise is not PENDING.

The then method allows you to add a callback function to the handlers array. The catch method is a shorthand for adding an error-handling callback function using the then method. The finally method allows you to execute a callback function after the promise is either fulfilled or rejected.

To use the MyPromise class, you can create a new instance and pass a callback function to the constructor. Inside the callback function, you can call the resolve() or reject() functions to change the state of the promise. You can then call the then(), catch(), or finally() methods on the promise to specify the callback functions that should be executed when the promise is fulfilled, rejected, or completed.

In the example code, we create a new MyPromise instance and pass a callback function that uses setTimeout() to delay the resolution of the promise for one second. We then call the then() method on the promise and pass a callback function that logs the resolved value to the console. When the promise is resolved, the callback function will be executed and the resolved value will be logged to the console.

References

--

--