Introduced in ES6, promises
are the big leap forward into asynchronous operations that JavaScript has needed for some time. However, in ES7 (or ESNext
, as the upcoming release is sometimes referred to), promises were dramatically improved with the introduction of async
functions and the await
operator. In short, an async
function simply defines and returns an asynchronous function, while the await
operator is used to wait for a Promise
object.
The secret sauce when using async
and await
together is that they effectively allow code to be written synchronously, while their behavior is still carried out behind the scenes as an asynchronous fashion. This is an extremely powerful feature, but the most noticeable effect when implementing async/await into your code is just how simple and easy to maintain the new syntax becomes.
In this article, we'll explore how to manage JavaScript async/await exception handling, including easy integration of the Airbrake-JS
library, so let's get to it!
Below is the full code sample we'll be using in this example. Feel free to copy and paste it into your own project to try it out or follow along:
class Book {
get author() {
return this.get('_author');
}set author(value) {
this.set('_author', value);
}get pageCount() {
return this.get('_pageCount');
}set pageCount(value) {
this.set('_pageCount', value);
}get title() {
return this.get('_title');
}set title(value) {
this.set('_title', value);
}
constructor(title, author, pageCount) {
this.author = author;
this.pageCount = pageCount;
this.title = title;
}/**
* General getter.
*
* @param property
* @returns {*}
*/
get(property) {
return this[property];
}/**
* General setter.
*
* Used to simulate different property set results,
* depending on property that is modified and existing values.
*
* @param property
* @param value
*/
set(property, value) {
return new Promise((resolve, reject) => {
if (property === '_title') {
// Simulate IO with 1 second delay.
setTimeout(() => {
let previousValue = this[property];
this[property] = value;
// Set title to new value no matter what.
if (typeof previousValue === 'undefined') {
resolve(`Updated Title to ${this.title}.`);
} else {
resolve(`Updated Title from '${previousValue}' to '${this.title}'.`);
}
}, 1000);
} else if (property === '_author') {
// Simulate IO with 1 second delay.
setTimeout(() => {
let previousValue = this[property];
// Set author to new value, if no author property is defined.
if (typeof previousValue === 'undefined') {
this[property] = value;
resolve(`Updated Title to ${this.title}.`);
} else {
// If author is already defined, reject update
// and throw new Error.
reject(new Error(`Cannot update Author from ${previousValue} to ${value}.`));
}
}, 1000);
}
});
}
/**
* Output Book to formatted string.
*
* @returns {string}
*/
toString() {
return `'${this.title}' by ${this.author} (${this.pageCount} pgs)`;
}
}
// main.js
require(['airbrakeJs/client'], function (AirbrakeClient) {
let airbrake = new AirbrakeClient({
projectId: 157536,
projectKey: 'e6b2c1bd63c0c26ab5751a7ce89d2757'
});testPromise();
testPromiseWithAirbrake(airbrake);
testAsyncAwait();
testAsyncAwaitWithAirbrake(airbrake);
});const testPromise = () => {
// Create new Book instance.
let book = new Book('The Stand', 'Stephen King', 1153);
// Output current title.
console.log(book.title);
// Set title new using promises.
book.set('_title', 'Promise Title').then(
// Handle resolve message.
(message) => console.log(message)
);
// Set new author using promises.
book.set('_author', 'Promise Author').then(
// Handle resolve message.
(message) => console.log(message),
// Handle reject message.
(err) => console.log(err)
);
};const testPromiseWithAirbrake = (airbrake) => {
// Create new Book instance.
let book = new Book('The Stand', 'Stephen King', 1153);
// Output current title.
console.log(book.title);
// Set title new using promises.
book.set('_title', 'Promise w/ Airbrake Title').then(
// Handle resolve message.
(message) => console.log(message)
);
// Set new author using promises.
book.set('_author', 'Promise w/ Airbrake Author').then(
// Handle resolve message.
(message) => console.log(message),
// Handle reject message.
(err) => {
// Handle error with Airbrake.
let promise = airbrake.notify(err);
promise.then(
(notice) => console.log('Airbrake Notice Id:', notice.id),
(noticeError) => console.log('Airbrake Notification Failed:', noticeError)
);
}
);
};const testAsyncAwait = async () => {
// With Async/Await, can use inline try-catch block.
try {
let book = new Book('The Stand', 'Stephen King', 1153);
await book.set('_title', 'Await Title');
await book.set('_author', 'Await Author');
} catch (err) {
console.log(err);
}
};
const testAsyncAwaitWithAirbrake = async (airbrake) => {
try {
let book = new Book('The Stand', 'Stephen King', 1153);
await book.set('_title', 'Await w/ Airbrake Title')
await book.set('_author', 'Await w/ Airbrake Author');
} catch (err) {
// Handle error with Airbrake, by awaiting promise from notify.
await airbrake.notify(err).then(
(notice) => console.log('Airbrake Notice Id:', notice.id)
);
}
};
// app.js
requirejs.config({
baseUrl: 'lib',
paths: {
app: '../app',
airbrakeJs: 'node_modules/airbrake-js/dist',
book: 'book'
}
});
requirejs(['app/main']);
<!DOCTYPE html>
<!-- index.html -->
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Promise, Async, and Await with Airbrake</title>
<script data-main="app" src="lib/require.js"></script>
</head>
<body>
</body>
</html>
For our example code we'll be using the basic Book
class:
class Book {
get author() {
return this.get('_author');
}set author(value) {
this.set('_author', value);
}get pageCount() {
return this.get('_pageCount');
}set pageCount(value) {
this.set('_pageCount', value);
}get title() {
return this.get('_title');
}set title(value) {
this.set('_title', value);
}
constructor(title, author, pageCount) {
this.author = author;
this.pageCount = pageCount;
this.title = title;
}/**
* General getter.
*
* @param property
* @returns {*}
*/
get(property) {
return this[property];
}/**
* General setter.
*
* Used to simulate different property set results,
* depending on property that is modified and existing values.
*
* @param property
* @param value
*/
set(property, value) {
return new Promise((resolve, reject) => {
if (property === '_title') {
// Simulate IO with 1 second delay.
setTimeout(() => {
let previousValue = this[property];
this[property] = value;
// Set title to new value no matter what.
if (typeof previousValue === 'undefined') {
resolve(`Updated Title to ${this.title}.`);
} else {
resolve(`Updated Title from '${previousValue}' to '${this.title}'.`);
}
}, 1000);
} else if (property === '_author') {
// Simulate IO with 1 second delay.
setTimeout(() => {
let previousValue = this[property];
// Set author to new value, if no author property is defined.
if (typeof previousValue === 'undefined') {
this[property] = value;
resolve(`Updated Title to ${this.title}.`);
} else {
// If author is already defined, reject update
// and throw new Error.
reject(new Error(`Cannot update Author from ${previousValue} to ${value}.`));
}
}, 1000);
}
});
}
/**
* Output Book to formatted string.
*
* @returns {string}
*/
toString() {
return `'${this.title}' by ${this.author} (${this.pageCount} pgs)`;
}
}
It has a handful of properties with explicit getters and setters (using the get
and set
keywords), but otherwise, the only special logic is the specific set(property, value)
and get(property)
methods. These are generalized methods which make it easier to create Promises
for our example. In particular, notice that the set(property, value)
method returns a new Promise(...)
object. To simulate an asynchronous action we're explicitly calling setTimeout()
and delaying by one second. Since we're returning a Promise
, we call the resolve(...)
method to indicate a successful call, while we invoke reject(...)
when the call has failed. In this case, reject(...)
is called only when attempting to update the author
property while an author
value already exists. This behavior could simulate business logic that requires rejecting a data record update when a value already exists, but, since it's an IO action, it must be performed asynchronously.
We'll begin with the most basic implementation of asynchronous operations, which is a plain old Promise
object. As we saw above, the Book.set(property, value)
method returns a Promise
object, so our testPromise()
function below creates a new Book
instance, sets the title
using the set(property, value)
method, then invokes the .then
method of the returned Promise
object, which is called once the Promise
comes back with a result:
const testPromise = () => {
// Create new Book instance.
let book = new Book('The Stand', 'Stephen King', 1153);
// Output current title.
console.log(book.title);
// Set title new using promises.
book.set('_title', 'Promise Title').then(
// Handle resolve message.
(message) => console.log(message)
);
// Set new author using promises.
book.set('_author', 'Promise Author').then(
// Handle resolve message.
(message) => console.log(message),
// Handle reject message.
(err) => console.log(err)
);
};
Since setting the title
property always succeeds, that will work and output the Promise
result to the log. However, the attempt to set a new author
value will fail, since an author already exists. This is confirmed by the log output, which occurs after the artificial one second delay:
Updated Title from 'The Stand' to 'Promise Title'.
Error: Cannot update Author from Stephen King to Promise Author.
Let's integrate our Promise
asynchronous methodology with the Airbrake-JS
module. We won't go over the setup process of Airbrake-JS
here, but be sure to check out the official documentation
for information on installing the library.
Once Airbrake-JS
is installed and we've created a new project on the Airbrake
dashboard, we can include the library in whatever manner is easiest. For this example, we're using requirejs
, so we start by defining the location of our app within the app.js
file, along with the location of the Airbrake-JS
module:
// app.js
requirejs.config({
baseUrl: 'lib',
paths: {
app: '../app',
airbrakeJs: 'node_modules/airbrake-js/dist',
book: 'book'
}
});
requirejs(['app/main']);
Now, within our main application code we require Airbrake-JS
, and then we can use the Airbrake client object to set our projectId
and projectKey
, both of which are found on the Airbrake dashboard:
// main.js
require(['airbrakeJs/client'], function (AirbrakeClient) {
let airbrake = new AirbrakeClient({
projectId: YOUR_PROJECT_ID,
projectKey: 'YOUR_PROJECT_KEY'
});// ...
testPromiseWithAirbrake(airbrake);
// ...
});
As you can see, we've opted to pass the Airbrake client object as a parameter to the testPromiseWithAirbrake(airbrake)
function, but we could just as easily instantiate it inside the function if we wish. Here we see the code of the aforementioned function:
const testPromiseWithAirbrake = (airbrake) => {
// Create new Book instance.
let book = new Book('The Stand', 'Stephen King', 1153);
// Output current title.
console.log(book.title);
// Set title new using promises.
book.set('_title', 'Promise w/ Airbrake Title').then(
// Handle resolve message.
(message) => console.log(message)
);
// Set new author using promises.
book.set('_author', 'Promise w/ Airbrake Author').then(
// Handle resolve message.
(message) => console.log(message),
// Handle reject message.
(err) => {
// Handle error with Airbrake.
let promise = airbrake.notify(err);
promise.then(
(notice) => console.log('Airbrake Notice Id:', notice.id),
(noticeError) => console.log('Airbrake Notification Failed:', noticeError)
);
}
);
};
In may look a bit complicated, but very little is different from our previous testPromise()
function. All we've added is the call to airbrake.notify(err)
. Since the notify(...)
method returns a Promise
object itself, we next call the then(...)
method on that object to perform various actions, depending if the Promise
succeeded or failed.
Executing the function above produces the following output, indicating that the attempt to set a new author
failed again, but this time we caught the error and processed it via Airbrake, which generated a new notification on the Airbrake dashboard:
Updated Title from 'The Stand' to 'Promise w/ Airbrake Title'.
Airbrake Notice Id: 000559d3-17a9-f4db-5760-7b1f87af2312
Sure enough, opening the Airbrake dashboard and looking at the Errors
panel shows a new error report, with an Error Message
of "Cannot update Author from Stephen King to Await w/ Airbrake Author."
. Neat!
Now, let's see what happens if we update our original testPromise()
function to use async/await
functionality. Here's the testAsyncAwait()
function to do just that:
const testAsyncAwait = async () => {
// With Async/Await, can use inline try-catch block.
try {
let book = new Book('The Stand', 'Stephen King', 1153);
await book.set('_title', 'Await Title');
await book.set('_author', 'Await Author');
} catch (err) {
console.log(err);
}
};
What should immediately jump out at you is just how little code is required. This function performs the same logic as the testPromise()
function, but requires nearly half the number of characters and lines of code. Moreover, there's no more need to mess with the complication of then(...)
method callbacks. Instead, await
handles that for us, by waiting for the result of the awaited
Promise
to ("pseudo-synchronously") move onto the next line within the same async
function.
The result of executing this function is just as we saw before, where the attempt to overwrite the author
property fails:
Error: Cannot update Author from Stephen King to Await Author.
Finally, let's pull it all together and see how to combine the advanced async/await
methodology with the simplicity of handling errors and exceptions via Airbrake. This is accomplished within the testAsyncAwaitWithAirbrake(airbrake)
method:
const testAsyncAwaitWithAirbrake = async (airbrake) => {
try {
let book = new Book('The Stand', 'Stephen King', 1153);
await book.set('_title', 'Await w/ Airbrake Title')
await book.set('_author', 'Await w/ Airbrake Author');
} catch (err) {
// Handle error with Airbrake, by awaiting promise from notify.
await airbrake.notify(err).then(
(notice) => console.log('Airbrake Notice Id:', notice.id)
);
}
};
Again, we've managed to cut down the lines/characters by half from the previous Promise-based
example that integrated with Airbrake. Best of all, since the Airbrake client's notify(...)
method returns a Promise
, we can simply prefix that call with the await
operator to halt execution at that point while waiting for the result. While we've then chosen to immediately invoke the then(...)
method of the returned Promise
object in this case, we could continue integrating async/await
all the way down the chain, to completely get rid of Promise.then(...)
method calls. For this case, however, then(...)
doesn't do anything that requires delayed processing/IO, so we can execute it immediately, once await
comes back.
The result of executing this code is just as we saw before:
Airbrake Notice Id: 000559d3-3bff-5c6e-2760-135cc8132852
And, just as before, a new error corresponding with this generated notification appears on the Airbrake dashboard, with the Error Message
of "Cannot update Author from Stephen King to Await w/ Airbrake Author."
That's just a small taste of the power of the Airbrake-JS
module and the newest async/await
methodology introduced in the upcoming JavaScript release!