Using RxJS Observables With JavaScript Async and Await

August 10, 2020
Written by
AJ Saulsberry
Contributor
Opinions expressed by Twilio contributors are their own

There are a number of resources for handling asynchronous tasks in JavaScript, each with its own strengths and suitability to specific tasks. Sometimes a single tool is all that’s necessary to accomplish a task, but there are programming challenges which can be handled more effectively by combining the capabilities of tools.

RxJS Observables enable you to operate on sequences of data, like the results of a REST API call, as if they were events, acting on new and updated data from the Observable object when the change occurs. They’re great tools to use when the timing of data events is unpredictable, like when you’re dealing with slow or occasionally unreliable web services.

JavaScript Promises are useful when you need a placeholder for the results of a task that might fail and need to respond differently depending on the task’s success or failure. Promise objects can be used with the JavaScript async and await keywords to hold the processing of a program’s main path of execution until a Promise is resolved. That’s great when your user interface behavior depends on the results of an asynchronous action.

So what do you do when your program needs to wait for the results of a slow, less-than-completely-reliable web service to determine what to do next?

You can use Observables with Promises and with async/await to benefit from the strengths of each of those tools. This post will show you how to code the combination of Observables, Promises, and async/await so your program can react to the state of an Observable and the resolution of a Promise, including errors, with timely user interface information.

Prerequisites

You’ll need the following resources to build and run the code presented in this post:

Node.js and npm – The Node.js installation will also install npm.

Visual Studio Code – or another IDE or editor

Git – for source code control or cloning the companion repository

Mocklets account – If you want to experiment with API mocking, create your own account. This tutorial’s code includes a link to a mock API set up by the author, so you don’t need an account to run the code. Free tier accounts are run-rate limited, so the one provided with this post might be too busy to respond to your GET requests if this post is popular on any given day.

Twilio account – Although not required for this tutorial, if you sign up for a free Twilio trial account with this link you will receive an additional $10 credit when you convert to a regular account.

In addition to these tools, you should also have a working knowledge of JavaScript basics and some exposure to JavaScript Promises, the async and await keywords, and RxJS Observables.

There is a companion repository containing the complete source code for this project on GitHub.

Setting up the Node.js project

Open a console window in the directory where you’d like to locate the project directory, then use the following command-line instructions to create the project directory, initialize a Git repo, and install the necessary npm modules:

md retry
cd retry
git init
npx license mit > LICENSE [Press Enter after the process finishes.]
npx gitignore node
npm init -y
git add -A
git commit -m "Initial commit"
npm install esm rxjs @akanass/rx-http-request

You may see a number of npm WARN messages, but they’re nothing to get hung about. 🍓

Coding the program

The demonstration program consists of an asynchronous function, getAndRetry and an anonymous asynchronous function that calls it. It only takes a few lines of code to show the power of this technique.

In the retry directory, create a new file named index.js and insert the following code:

import { RxHR } from '@akanass/rx-http-request';
import { retry, catchError, tap } from 'rxjs/operators';
import { throwError } from 'rxjs';

async function getAndRetry(url, retryCount) {
  return RxHR.get(url).pipe(
    tap(output => {
      if (output.response.statusCode >= 400)
        throw new Error(`StatusCode: ${output.response.statusCode}`);
    }),
    catchError(error => {
      console.log('Tried ' + url + ' Got ' + error);
      return throwError(error);
    }),
    retry(retryCount),
  ).toPromise();
}

(async () => {
  try {
    const results = await getAndRetry('https://api.mocklets.com/mock68043/', 10);
    console.log(results.body);
  } catch (error) {
    console.log('Retries were exhausted before a successful response was received. :-(');
  }
})();

That’s it!

Testing the completed app

It will be easier to understand what the code is doing if you see it in action, so run the program by executing the following command-line instruction in the retry directory:

node -r esm index.js

Then run it again. And again. And even more times if you’re having fun.

The code will try up to 11 times to get a successful response from a mock REST API. If it encounters an error it will log the error and retry, up to the retry limit of 10; the first attempt isn’t a retry. If a success response is received the body of the response will be logged to the console and the program will stop.

You might notice that some messages take longer than others to appear. A delay of three seconds is built into the 503 response.

The mock API will randomly return three responses:

Status: 200 OK
Content-Type: text/plain
Body: Congratulations! Everything's coming up 200 with this API.

Status: 400 Bad Request
Content-Type: application/json
Body: {}

Status: 503 Service Unavailable
Content-Type: application/json
Body: {}

Because the API is designed to be wobbly, your results will vary. In six trial runs it produced the following:

PS C:\Projects\ecartman\retry> node -r esm index.js
Congratulations! Everything's coming up 200 with this API.
PS C:\Projects\ecartman\retry> node -r esm index.js
Tried https://api.mocklets.com/mock68043/ Got Error: StatusCode: 400
Tried https://api.mocklets.com/mock68043/ Got Error: StatusCode: 400
Tried https://api.mocklets.com/mock68043/ Got Error: StatusCode: 503
Congratulations! Everything's coming up 200 with this API.
PS C:\Projects\ecartman\retry> node -r esm index.js
Congratulations! Everything's coming up 200 with this API.
PS C:\Projects\ecartman\retry> node -r esm index.js
Tried https://api.mocklets.com/mock68043/ Got Error: StatusCode: 400
Congratulations! Everything's coming up 200 with this API.
PS C:\Projects\ecartman\retry> node -r esm index.js
Tried https://api.mocklets.com/mock68043/ Got Error: StatusCode: 400
Congratulations! Everything's coming up 200 with this API.
PS C:\Projects\ecartman\retry> node -r esm index.js
Tried https://api.mocklets.com/mock68043/ Got Error: StatusCode: 503
Tried https://api.mocklets.com/mock68043/ Got Error: StatusCode: 400
Congratulations! Everything's coming up 200 with this API.
PS C:\Projects\ecartman\retry> 

If all the retries are exhausted the program will let you know and quit. To see that more easily, change the 10 in the following line of code in the anonymous function to a 1:

const results = await getAndRetry('https://api.mocklets.com/mock68043/', 1);

It may take a few tries, but you’ll eventually see output like this:

Tried https://api.mocklets.com/mock68043/ Got Error: StatusCode: 400
Tried https://api.mocklets.com/mock68043/ Got Error: StatusCode: 503
Retries were exhausted before a successful response was received. :-(

There are a few points of interest in how this output is produced:

  • The success, error, and exhaustion messages come from different places in the program.
  • The await-ed call to the async function getAndRetry finishes before execution resumes in the anonymous function.
  • The only output that isn’t produced by an error handler is the logging of a successful response.

Understanding the code

Once you’ve gotten a sense for how the program behaves you can dig into the specifics of the code and get a better understanding of how the asynchronous technologies work.

(async () => {
  try {
    const results = await getAndRetry('https://api.mocklets.com/mock68043/', 10);
    console.log(results.body);
  } catch (error) {
    console.log('Retries were exhausted before a successful response was received. :-(');
  }
})();

The top level of execution in this demonstration app is the anonymous function marked with the async keyword. It calls the getAndRetry function and awaits its results before proceeding. When the getAndRetry function successfully returns a value the body of the HTTP response is sent to the console.

Because the function is wrapped in a try…catch structure, errors thrown by getAndRetry will be caught in the catch block. In the narrow context of this demonstration, the error that will be thrown by the getAndRetry function will be when the Promise returned by the getAndRetry function is rejected.

async function getAndRetry(url, retryCount) {
  return RxHR.get(url).pipe(
    tap(output => {
      if (output.response.statusCode >= 400)
        throw new Error(`StatusCode: ${output.response.statusCode}`);
    }),
    catchError(error => {
      console.log('Tried ' + url + ' Got ' + error);
      return throwError(error);
    }),
    retry(retryCount),
  ).toPromise();
}

To understand why, take a look at the getAndRetry function. The function uses the RxHR library to create an Observable from the response received from the target address specified by the url parameter using its .get method.

Because RxHR creates an RxJS Observable, you can use RxJS operators with it. Operators are functions. There are two kinds: 1) those that take Observables as input and return Observables as output and 2) those that are used to create a new observable.

The pipe function takes the original Observable and uses it in all the operators within the scope of the .pipe. Within the .pipe the RxJS tap operator is used to perform a side action that doesn’t affect the Observable’s state or contents. The term function in RxJS refers to functions which produce output.

The body of the tap operator is used to check the HTTP status code returned from the endpoint at the url address. If it’s equal to, or greater than, 400 the tap operator throws an error.

The error thrown by the tap operator is caught by the RxJS catchError operator, which sends the url value and the error to the console. The form of the error is the url tried and the output.response.statusCode received.

The catchError operator can be used to return a new Observable when an error is thrown or continue with the existing Observable. In getAndRetry the operator continues with the original Observable, the one that calls the address in url.

The RxJS retry operator returns the original Observable minus the error until the number of attempts specified in the retryCount parameter is exceeded. If there hasn’t been a successful attempt at that point the error is returned by the Observable.

Of course, if the value of output.response.statusCode is less than 400 the response from the url address is emitted by the Observable. At that point the RxJS Observable toPromise function is used to convert the RxJS Observable to a JavaScript Promise.

If the Promise is fulfilled, synchronous processing resumes and the contents of results.body are sent to the console. Using the Mocklets API provided in this post and the companion repository, the response will be:

Congratulations! Everything's coming up 200 with this API.

If the response from getAndRetry is a rejected Promise, that’s caught as an error and the console receives a disappointed message:

Retries were exhausted before a successful response was received. :-(

Either of those outcomes concludes program execution.

Further exploration

Here are a few suggestions for ways you can use this demonstration application to better understand asynchronous JavaScript and how RxJS Observables can be used with async and await:

  • Set breakpoint in Visual Studio Code, or another capable IDE, to examine the state of the Observables and the Promise as the code executes.
  • Add other functions that create RxJS Observables and return Promises.
  • Create a structure where a Promise is either fulfilled or rejected depending on the condition of multiple Observables.
  • Add structures that follow the sample code provided in the RxJS and RxHR documentation.
  • Create your own Mocklets API to simulate real-world REST API interaction.
  • Sign up for a free Twilio trial account and use this program to check the Twilio Status API.

If you dig into the RxJS and RxHR documentation you’ll find there are a number of ways to write functions that use those technologies. You can experiment with those as well.

Summary

In this post you built a Node.js application to demonstrate how to use RxJS Observables with JavaScript Promises and the async and await keywords. The program you built demonstrated how Mocklets can be used to create dynamic REST API simulations for application development and testing. You saw how to use a number of RxJS operators and how to convert an RxJS Observable to a JavaScript Promise.

Additional resources

There are a number of posts on the Twilio blog which provide more information about asynchronous JavaScript, including RxJS Observables and Promises:

Asynchronous JavaScript: Understanding Callbacks

Asynchronous JavaScript: Introduction to JavaScript Promises

Asynchronous JavaScript: Advanced Promises with Node.js

Asynchronous JavaScript: Introducing ReactiveX and RxJS Observables

Asynchronous JavaScript: Using RxJS Observables with REST APIs in Node.js

Asynchronous JavaScript: Introducing async and await

Asynchronous JavaScript: Using Promises With REST APIs in Node.js

If you’re new to asynchronous JavaScript, or JavaScript in general, you’ll get a comprehensive overview of the asynchronous tools by reading these posts in the order they’re listed.

You might also want to refer to the following canonical sources of documentation:

JavaScript – The MDN Web Docs are a good resource for learning to write JavaScript for browsers and Node.js.

RxJS – This is the documentation JavaScript implementation of ReactiveX. You might find it helpful in the event “Think of RxJS as Lodash for events.” doesn’t explain everything.

AJ Saulsberry is a technical editor at Twilio. Get in touch with him if you have been “there and back again” with a software development topic and want to make money writing about it for the Twilio blog.