How to Throw Errors From Async Functions in JavaScript?
It is possible to throw errors from async functions in JavaScript? The topic has been covered hundred of times but let's see it from a TDD standpoint.
Answer the question without looking at Stackoverflow. If you know the answer, well I'm impressed. If not that's cool too. Keep reading and you'll find it!
How to Throw Errors From Async Functions in JavaScript: what you will learn
In the following post you'll learn:
- how to throw errors from async functions in JavaScript
- how to test exception from async functions with Jest
How to Throw Errors From Async Functions in JavaScript: requirements
To follow along you should have:
- a basic understanding of JavaScript and ES6
- a working installation of Node.Js, and Jest
How to Throw Errors From Regular Functions in JavaScript
"Use exceptions rather than return codes" (Clean code). Throwing errors is a best practice for dealing with unknowns. The same rule applies for every modern language: Java, JavaScript, Python, Ruby.
You can throw errors from a function, consider the following example in JavaScript:
function upperCase(name) {
if (typeof name !== "string") {
throw TypeError("name must be a string");
}
return name.toUpperCase();
}
module.exports = upperCase;
And here is the test for it (I'm using Jest):
"use strict";
const assert = require("assert");
const upperCase = require("../function");
describe("upperCase function", () => {
test("it throws when name is not provided", () => {
assert.throws(() => upperCase());
});
test("it throws when name is not a string", () => {
assert.throws(() => upperCase(9));
});
});
You can throw errors from ES6 classes too. I always throw in the constructor for unexpected values when writing classes in JavaScript. A simple example:
class Person {
constructor(name) {
if (typeof name !== "string") {
throw TypeError("name must be a string");
}
this.name = name;
}
// some method here
}
module.exports = Person;
And here is the test for the class:
"use strict";
const assert = require("assert");
const Person = require("../index");
describe("Person class", () => {
test("it throws when name is not provided", () => {
assert.throws(() => new Person());
});
test("it throws when name is not a string", () => {
assert.throws(() => new Person(9));
});
});
The test indeed passes:
PASS test/index.test.js
Person class
✓ it throws when name is not provided (1ms)
✓ it throws when name is not a string
Neat! So everything works as expected whether you're throwing from a regular function or from a class constructor (or from a method).
What if I want to throw an error from an async function? Can I still use assert.throws in my test?
Let's find out.
How to Throw Errors From Async Functions in JavaScript: testing exceptions
So you know JavaScript async functions right? Given the previous class:
class Person {
constructor(name) {
if (typeof name !== "string") {
throw TypeError("name must be a string");
}
this.name = name;
}
// some method here
}
module.exports = Person;
suppose you want to add an async method for fetching data about that person. Such method takes a url. If the url is not a string we throw an error like we did in the previous example.
Let's update the class:
class Person {
constructor(name) {
if (typeof name !== "string") {
throw TypeError("name must be a string");
}
this.name = name;
}
async getData(url) {
if (typeof url !== "string") {
throw TypeError("url must be a string");
}
// const response = await fetch(url)
// do stuff
}
}
module.exports = Person;
What will happen if I run the code? Let's try:
const Person = require("../index");
const valentinogagliardi = new Person("valentinogagliardi");
valentinogagliardi.getData();
Here it is:
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): TypeError: name must be a string
DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Unsurprisingly the async method raises a Promise rejection but it doesn't throw in the strict sense. The error is wrapped inside a Promise rejection.
In other words I cannot use assert.throws
for testing it. Let's confirm with a test:
"use strict";
const assert = require("assert");
const Person = require("../index");
describe("Person methods", () => {
test("it throws when url is not a string", () => {
const valentinogagliardi = new Person("valentinogagliardi");
assert.throws(() => valentinogagliardi.getData());
});
});
The test fails as expected!
FAIL test/index.test.js
Person methods › it throws when url is not a string
assert.throws(function)
Expected the function to throw an error.
But it didn't throw anything.
Message:
Missing expected exception.
So? What's the catch? (No pun intended).
How to Throw Errors From Async Functions in JavaScript: catch me if you can
Async functions and async methods do not throw errors in the strict sense. Async functions and async methods always return a Promise, either resolved or rejected.
You must attach then()
and catch()
, no matter what. (Or wrap the method inside try/catch
). A rejected Promise will propagate up in the stack unless you catch it.
As for the test here's how it should be:
"use strict";
const assert = require("assert");
const Person = require("../index");
describe("Person methods", () => {
test("it rejects when url is not a string", async () => {
expect.assertions(1);
const valentinogagliardi = new Person("valentinogagliardi");
await expect(valentinogagliardi.getData()).rejects.toEqual(
TypeError("url must be a string")
);
});
});
We must test not the plain exception, but the rejects with a TypeError
. Now the test passes:
PASS test/index.test.js
Person methods
✓ it rejects when url is not a string
How about the code? To catch the error you would refactor like so:
const Person = require("../index");
const valentinogagliardi = new Person("valentinogagliardi");
valentinogagliardi
.getData()
.then(res => res)
.catch(err => console.error(err));
Now the exception will show up in the console:
TypeError: url must be a string
at Person.getData (/home/valentino/Documenti/articles-and-broadcasts/throw-from-async-functions-2018-04-02/index.js:12:13)
at Object.<anonymous> (/home/valentino/Documenti/articles-and-broadcasts/throw-from-async-functions-2018-04-02/index.js:22:4)
// ...
There is an important thing to note if you like more try/catch
. The following code won't catch the error:
const Person = require("../index");
async function whatever() {
try {
const valentinogagliardi = new Person("valentinogagliardi");
await valentinogagliardi.getData();
// do stuff with the eventual result and return something
} catch (error) {
throw Error(error);
}
}
whatever();
Remember: a rejected Promise will propagate up in the stack unless you catch it. To catch the error properly in try/catch
you would refactor like so:
async function whatever() {
try {
const valentinogagliardi = new Person("valentinogagliardi");
await valentinogagliardi.getData();
// do stuff with the eventual result and return something
} catch (error) {
throw Error(error);
}
}
whatever().catch(err => console.error(err));
That's how it works.
How to Throw Errors From Async Functions in JavaScript: wrapping up
To recap:
Throwing error from an async function won't spit out a "plain exception".
Async functions and async methods always return a Promise, either resolved or rejected.
To intercept exceptions from async functions you must use catch()
.
Here are the rules for testing exceptions in Jest:
- use
assert.throws
for testing exceptions in normal functions and methods. - use expect + rejects for testing exceptions async functions and async methods.
Thanks for reading!