DEV Community

Chris Andrade
Chris Andrade

Posted on • Updated on

Asynchronous Programming with JavaScript/TypeScript

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();
Enter fullscreen mode Exit fullscreen mode

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.');

            }
        });
Enter fullscreen mode Exit fullscreen mode

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);
    }
};

Enter fullscreen mode Exit fullscreen mode

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);
    });
}
Enter fullscreen mode Exit fullscreen mode

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');
})

Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

A more useful promise chain example

The code below represents requests to:

  1. Get a User
  2. Get Permissions for a user
  3. Get the Current Time
  4. 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);
      });
}
Enter fullscreen mode Exit fullscreen mode

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');
}

Enter fullscreen mode Exit fullscreen mode

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'));
      }
    });
}

Enter fullscreen mode Exit fullscreen mode

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'));
}
Enter fullscreen mode Exit fullscreen mode

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');
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.

Image description


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));
}

Enter fullscreen mode Exit fullscreen mode

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.

Image description


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)));
}

Enter fullscreen mode Exit fullscreen mode

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.

Image description

// 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), []);
}

Enter fullscreen mode Exit fullscreen mode

reduce
reduce is similar to the javascript Array.reduce method. It consists of an accumlated value, and iterated value.

Image description

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
      []);
}
Enter fullscreen mode Exit fullscreen mode

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()));
}
Enter fullscreen mode Exit fullscreen mode

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? 
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

Converting from Promise to Observable

/**
 * Use the from operator to convert from promise to observable
 */
function fromUserPromise(): Observable<any> {
  return from(getUserPromise());
}

Enter fullscreen mode Exit fullscreen mode

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)