Before Promises there were callbacks
Event handling before promises were introduced were done via callbacks.
Before libraries and fetch there was XMLHttpRequest
Early JavaScript involved usage of call back functions. In the example below the developer needs to write an onreadystatechange function to handle when an asyncronous request was completed. The structure below is similar analogous to the way we write event listeners for DOM elements.
var xhttp = new XMLHttpRequest();
// create a callback function which will be fulfilled when the request completes
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
// Typical action to be performed when the document is ready:
document.getElementById("demo").innerHTML = xhttp.responseText;
}
};
xhttp.open("GET", "filename", true);
xhttp.send();
Abstracting away the XMLHttpRequest
Various libraries created their owns mechanisms to make code like the above easier. For example ExtJS, see https://www.extjs-tutorial.com/extjs/ajax-request-in-extjs4
Ext.Ajax.request({
url: '/api/students',
method: 'GET',
timeout: 60000,
params:
{
id: 1 // loads student whose Id is 1
},
headers:
{
'Content-Type': 'application/json'
},
success: function (response) {
},
failure: function (response) {
Ext.Msg.alert('Status', 'Request Failed.');
}
});
Writing our own XMLHttpRequest Library
We begin to see some improved code re-use and abstraction.
Can we make our own call-back style library to make XML Http Requests? Sure Why Not?
var Request = {
get: function (url, success, failure) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
// 4 means we're done see https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState
if (this.readyState === 4) {
if (this.statusCode >= 200 && this.statusCode < 300) {
success(xhr.responseText);
} else {
failure(xhr.responesText);
}
}
};
};
Request.get('/services/something',
function (response) {
console.log(response);
},
function (err) {
console.error(err);
}
};
Callback Hell
There some potential issues. When we start having to chain calls together, it gets messy. The example below uses jQuery to write functions to call various REST api.
function getUser(success, failure) {
$.get('services/user', function (response) {
try {
success(JSON.parse(response));
} catch (err) {
failure(response);
}
});
}
function getPermissions(userId, success, failure) {
$.get('services/permissions/' + userId, function (response) {
try {
success(JSON.parse(response));
} catch (err) {
failure(err);
}
});
}
function getCurrentTime(success, failure) {
$.get('services/currentTime', function (response) {
try {
success(JSON.parse(response));
} catch (err) {
failure(err);
}
});
}
// lets get the user
// check if they have permission
// if they do then get the time
function getTimeForUser(callback) {
getUser(
function (user) {
getPermissions(user.userId,
function (permissions) {
if (permissions.canDoThing) {
getCurrentTime(
function (currentTime) {
callback(null, currentTime);
},
function (error) {
callback(error, null);
}
);
}
},
function (error) {
console.error('Failed to get permissions);
callback(error, null);
})
},
function (user) {
console.error('Couldn\'t get the user');
callback(error, null);
});
}
Promises
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
The Promise object represents the eventual completion (or failure) of an asynchronous operation and its resulting value.
It's easy to see the challenge of maintaining asynchronous codes using only callbacks. Promises provide a new mechanism for transfering/representing asynchronous state. Yes there are callbacks, but they are abstracted away in a variable we can use later.
Basic structure
The basic structure of a promise is then
and catch
. See example below:
function getThing() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('thing');
}, 1000);
});
}
const prom = getThing();
prom.then(
// success handler
function (thing) {
console.log('I got the thing');
},
// second argument is failure, using this is considered an anti-pattern
// see anti-patterns section for promises
function (error) {
console.error('I failed');
}
// we can catch errors like this too
).catch((error) => {
console.error('I failed');
})
Promises make things cleaner ( a bit)
Promises return an object which represents a value to be eventually fulfilled. It allows us to chain multiple asyncronous parts of code together more cleanly. Promise chains follow the basic structure of then
, then
, catch
Basic Promise Chain Structure
Chaining promises together allows us to more cleanly
write a sequence of asyncronous actions.
// then
Promise.resolve().then(
// then
() => Promise.resolve()).then(
// catch
() => Promise.reject('error')).catch(
(err) => console.error('An error occured =>', err));
A more useful promise chain example
The code below represents requests to:
- Get a User
- Get Permissions for a user
- Get the Current Time
- Composing the above 3 functions together to make another function.
function getUser() {
return fetch('services/user').
then((response) => response.json());
}
function getPermissions(userId) {
return fetch('services/permissions/' + userId).
then((response) => response.json());
}
function getCurrentTime() {
return fetch('services/currentTime').
then((response) => response.json());
}
function getTimeForUser() {
// get the user
getUser().then(
// get the permissions
(user) =>
// get permissions
getPermissions(user.userId)).then(
(permissions) => {
if (permissions.canDoThing) {
return true;
} else {
return false
}
}).then(
// if they have permission
(hasPermission) => {
if (hasPermission) {
return getCurrentTime();
} else {
return Promise.reject('doesn\'t have permission');
}
// catch - all
}).catch((err) => {
console.error('Failed to get time');
return Promise.reject(err);
});
}
Promise Anti-Patterns
Although Promises clearly have the ability to improve things here are a few mistakes that are commonly made.
see https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns#the-deferred-anti-pattern
The Deferred anti-pattern
This is when you are creating promises for no reason when you already have a promise
function getThingsDeferredAntiPattern() {
return new Promise((resolve, reject) => {
fetch('/services/thing').then(
(thing) => resolve(thing)).
catch((err) => reject(err));
});
}
function getThings() {
return fetch('/services/thing');
}
Nesting Promises
This is often the fallback of developers who had previously worked with callbacks.
/**
* NO
*/
getCurrentTimeForUserNested() {
return getUser().then(
(user) => {
getPermissions(user.userId).then(
(permissions) => {
if (permissions.canDoThing) {
return getCurrentTime();
}
})
});
}
/**
* Using promises properly, remember that a promise is an object
* so we can chain then more neatly
*/
getCurrentTimeForUser() {
return getUser().then(
(user) => this.getPermissions(user.userId)).then(
(permissions) => permissions.canDoThing).then(
(canDoThing) => {
if (canDoThing) {
return getCurrentTime();
} else {
return Promise.reject(new Error('User doesn't have permission'));
}
});
}
The .then(success, fail) anti-pattern
It's considered bad practice to use the second argument of a then
statement. Using these tend to devolve into callback chains.
Using the above example belowL
getCurrentTimeForUser() {
return getUser().then(
(user) => {
return getPermissions(user.userId).then(
(permissions) => {
if (permissions.canDoThing) {
return this.getCurrentTime();
} else {
return Promise.reject('User doesn\'t have permission');
}
},
(error) => Promise.reject('Couldn\'t get permissions =>', error));
},
(error) => Promise.reject('Couldn\'t get user'));
}
Async-Await Syntactic Sugar
The async and await keywords enable asynchronous, promise-based behavior to be written in a cleaner style, avoiding the need to explicitly configure promise chains.
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
Even though the readability is greatly improved it still doesn't read as smoothly as ordinary code. Async-Await was introduced as syntactic sugar that improves the readability of promises.
The example below expands on the getCurrentTimeForUser method and writes it in a cleaner way.
Revisiting our Promise example
function getUser() {
return fetch('services/user').
then((response) => response.json());
}
function getPermissions(userId) {
return fetch('services/permissions/' + userId).
then((response) => response.json());
}
function getCurrentTime() {
return fetch('services/currentTime').
then((response) => response.json());
}
// lets re-write this using promises
async function getTimeForUser() {
try {
const user = await getUser();
const permissions = await getPermissions(user.userId);
if (permissions.canDoThing) {
const time = await getCurrentTime();
return time;
} else {
return Promise.reject(new Error('Don\'t have permission to do thing'));
}
} catch (err) {
console.error('Failed to get the time for the user');
}
}
Which to use? It depends
There are advantages and disadvantages of using the Promise
style or the Async-Await
style.
Comparing Promises and Async-Await
function getRandomNumber() {
return fetch('/services/random').
then(response => response.json());
}
// sometimes using async-await is more wordy
function getRandomNumberAlt() {
const response = await fetch('/services/random');
return response.json();
}
/**
* Get numbers using promise style
*/
function getNumbersPromise() {
const nums = [];
for (let i = 0, end = 10; i < end; i++) {
getRandomNumber().then((num) => {
// to ensure proper ordering we
// use closure
// i think this should work?
nums[i] = num;
});
}
}
/**
* Get numbers using async-await
* Using async await can result in more readable code
*/
function getNumbersAsync() {
const nums = [];
for (let i = 0, end = 10; i < end; i++) {
// better this is more obvious
// since we will wait to get the random
// number before pushing it into the array
const num = await getRandomNumber();
nums.push(num);
}
}
A Brief overview RxJS Observables
Observables are the next iteration of Async Programming in JavaScript/TypeScript.
A representation of any set of values over any amount of time.
see https://rxjs.dev/api/index/class/Observable
Promises vs. Observables
A key distinction between Promises
and Observables
is that Observables can represent 0, 1 or more emitted values. This is similar to an event that can occur many times or not at all, like when the user clicks a button.
Observables are Lazy
Observables are lazy Push collections of multiple values.
see https://rxjs.dev/guide/observable
What does it practically mean to be lazy?
Why is it that nothing is executed when I call an observable function? Newcomers to Observables often are confused by the lazy behavior of Observables.
// a normal http request in angular (or whatever)
getThing(): any {
return http.get('services/thing');
}
load(): void {
// this will not invoke an ajax request
// because we have not subscribed
service.getThing();
}
loadWorking(): void {
// this will invoke an ajax request
// because we have subscribed for changes
service.getThing().subscribe((data: any) => {
this.doSomething(data);
}
}
Observables are like an event, and occur 0 or more times.
Observables are distinct from Promises in their ability to emit multiple values.
Given the following observable
const observable: Observable<any> = new Observable((subscriber: Subscriber) => {
subscriber.next(0);
subscriber.next(1);
subscriber.next(2);
});
Get only the first value
I only care about the very first value emitted by an observable.
observable.pipe(first()).subscribe((value: any) => {
console.log({ first: value });
});
// outputs 0
Get only the last value
I only care about the most recent value emitted by an observable. last May be a misnomer since it refers to:
the last item from the source Observable that satisfies the predicate.
see https://rxjs.dev/api/operators/last
/**
* I just want the last value emitted
*/
observable.pipe(last()).subscribe((value: any) => {
console.log({ last: value });
});
// outputs 2
A note about toPromise
The toPromise method returns the value of a completed observable. This can confusing to some users who expect an observable to emit a value but it does not.
The code below will never execute the console.log statement because the observable never completed.
observable.toPromise().then((value: any) => {
console.log({ prom: value });
});
// never outputs because toPromise only sends value when an observable is complete
toPromise() will only resolve when an observable value has completed
cosnt completed: Observable<any> = new Observable((subscriber: Subscriber) => {
subscriber.next(0);
subscriber.next(1);
subscriber.next(2);
subscriber.complete();
});
completed.toPromise().then((value: any) => {
console.log({ completed: value });
});
// outputs 2
Mapping Operators
The various mapping operators allow a you to transform emitted values in an observable.
map
The base map operator allows you to transform an emitted value.
getCurrentUser(): Observable<any> {
return http.get('/services/currentUser');
}
// take the observable returned by getCurrentUser
getCurrentUserId(): Observable<string> {
return getUser().pipe(
// use map operator to map the user into some other value
map((user: any) => user.userId));
}
mergeMap/flatMap
The flatMap or mergeMap operator is similar to the base map
operator. It takes a source observable's emitted values, and then merges them with another observable value to produce a resultant observable value.
getPermissions(userId: string): Observable<Permissions> {
return http.get(`/services/permissions/{$userId}`);
}
getCurrentUserPermissions(user: any): Observable<any> {
// get the user observable
const user: Observable<any> = getCurrentUser();
return user.pipe(
// use flatmap to combine results of the getPermissions()
// observable and user observable
flatMap((user: any) => getPermissions(user.userId)));
}
The key takeaway is that when you want to use another observable value, when mapping use the flatMap
or mergeMap
operator.
concatMap
concatMap allows you to take a collection of values and emit them as distinct values.
// ids => [1, 2, 3]
getUsers(ids: string[]): Observable<User[]> {
return from(ids).pipe(
// takes the list of ids and make multiple user calls
concatMap((userId: string) => getUser(userId)))),
// convert it to an array of users, we'll explain this later
reduce((acc: User[], user: User) => acc.push(user), []);
}
reduce
reduce is similar to the javascript Array.reduce
method. It consists of an accumlated value, and iterated value.
getUsers(ids: string[]): Observable<User[]> {
return from(ids).pipe(
// takes the list of ids and make multiple user calls
concatMap((userId: string) => http.get(`services/user/${userId}`))),
// convert it to an array of users, we'll explain this later
reduce(
// our accumulator function
(acc: User[], user: User) => {
acc.push(user);
return acc;
},
// our starting value
[]);
}
There are many more mapping and other operators, we've only just touched a few of them.
Functional Programming with observables
Using observable allows us to compose our observables from other observables
function getUser(): Observable<any> {
return http.get('services/user');
}
function getPermissions(userId): Observable<any> {
return http.get('services/permissions/' + userId);
}
function getCurrentTime(): Observable<number> {
return http.get('services/currentTime');
}
getTimeForUser(): Observable<number> {
return getUser().pipe(
// after we get the user return the user id
map((user: any) => user.userId),
// pass the user id to another observable
flatMap((userId: string) => getPermissions(userId)),
// conditionally continue
filter((permissions: any) => permissions.canDoThing),
// get the current time
flatMap(() => getCurrentTime()));
}
see https://rxjs.dev/guide/operators for a complete list of operators
Composing Software: An Exploration of Functional Programming and Object Composition in JavaScript by Eric Elliot is a comprehensive overview of functional techniques
Observable Anti-Patterns
Nested Subscriptions
getTimeForUser(): any {
// whats wrong here?
return getUser().subscribe((user: any) => {
getPermissions().subscribe((permissions: any) => {
if (permissions.canDoThing) {
return getCurrentTime();
}
})
});
}
// what if any of these observables emit multiple values, or never complete?
// do you see the memory leak?
Converting to Promise
// NO!
function getUserPromise(): Promise<any> {
return new Promise((resolve) => {
// do you see the memory leak?
getUser().subscribe((user) => {
resolve(user);
});
});
}
// Use built-in functions
function getUserPromiseWorking(): Promise<any> {
// see caveat about using toPromise above
return getUser().toPromise();
}
Converting from Promise to Observable
/**
* Use the from operator to convert from promise to observable
*/
function fromUserPromise(): Observable<any> {
return from(getUserPromise());
}
Conclusion
Modern JavaScript and TypeScript provide powerful tools for asyncronous programming. Promises and Async-Await are built in mechanisms that originated from the community and were so widely adopted they were introduced into the ECMA standards. RxJS Observables are a powerful tool that build on Promises and Async-Await and extend them to create some useful mechanisms that are functionally oriented.
Top comments (0)