On my past few weekend Twitch streams (twitch.tv/irreverentmike by the way) I've been working on a browser-based guitar tuner, to make usre of silly domain name I bought a year or so ago, guithub.org.
Working with Web APIs for Audio is super interesting, and has given me an opportunity to research and learn about lots of great stuff that's built into modern web browsers that I hadn't used much before, like the Canvas API and the Web Audio API.
It also requires me to use lots of asynchronous code. Both Web Audio and Canvas require async
to function, and as a result I've been using a lot of promises in my code. As I write and refactor the code for my pet project, I've found myself running into lots of errors relating to the setup and use of async
stuff.
The basics of async / await in JavaScript
Executing code with async
/ await
in JavaScript code requires a small amount of setup. At its most basic, it looks like this:
// Functions which use await to execute code must be declared with the "async" keyword
async function foo() {
return await bar();
}
// written another way
const foo = async () => {
await bar();
};
The async
keyword is used to adorn the parent function, to let JavaScript know that somewhere inside the function you're going to be await
ing something from another function call.
The await
keyword is used to tell JavaScript that the function you're calling on that line is asynchronous, and that it will be waiting for something to happen before it can continue.
What happens when you forget to use async
Both of these ingredients are required for async
/ await
to work, but drastically different things happen if you forget one or the other. If you forget to add async
- it's very likely that your code won't run at all. Somewhere along the line, the JavaScript interpreter will crash, and tell you that you're trying to use await
in a function that isn't marked as async
.
What is a floating promise?
A floating promise is an async function that is called without use of the await
keyword.
In many cases, if you forget to include await
, your IDE/linter/interpreter won't fail at all, because you technically haven't done anything wrong. You can call an async function and not wait for it... this essentially creates a Promise
but doesn't wait for it to resolve or reject. You'll effectively never hear back from it, and it may not even continue to execute.
I'll take an example of what this looks like from the docs page for eslint-plugin-no-floating-promise
, which you can find on npm and GitHub:
async function writeToDb() {
// asynchronously write to DB
}
writeToDb(); // <- note we have no await here but probably the user intended to await on this!
When writeToDb()
is called, it's not waiting for anything to happen, and it's not returning a Promise
to the caller. Instead, the app will continue on its merry way without necessarily throwing any exceptions... and very likely without writing to the database at all.
It gets worse if you're relying on the return value from an async function:
async function createNewRecordInDb(input) {
// asynchronously create new record in DB;
let newRecord = await blah(input.name, input.email);
return newRecord;
}
const entry = createNewRecordInDb({
name: 'John Doe',
email: 'foo@bar.com'
);
console.log('welcome to earth a brand new entry', entry)
This is a problem, as the code operates assuming you've gotten back a value from a function that's actually still executing. This is called a floating promise, and it's a somewhat common mistake to make. It's a promise that is not being used by the rest of the code, so it's not being resolved.
If you use JavaScript: eslint-plugin-no-floating-promise Saves the day
As mentioned above, the eslint-plugin-no-floating-promise rule is a great way to make sure you don't accidentally forget to use await
in your async functions. If you're working in JavaScript and your project already uses eslint, adding eslint-plugin-no-floating-promise
is as easy as adding the plugin to your .eslintrc
config file:
{
"plugins": ["no-floating-promise"]
}
and then adding the rule to your rules
object:
{
"rules": {
"no-floating-promise/no-floating-promise": 2
}
}
You can see more details in the docs for eslint-plugin-no-floating-promise.
If you use TypeScript: @typescript-eslint/no-floating-promises
already exists!
If you're working in TypeScript, there's already a handy solution baked into @typescript-eslint
- just activate the rule @typescript-eslint/no-floating-promises
and you're good to go!
{
/* ... */
"rules": {
"@typescript-eslint/no-floating-promises": "error"
}
}
Conclusion
This is a really great way to protect yourself from an asynchronous programming issue in JavaScript and Typescript that can be extremely frustrating to debug if you're not actively looking for it. While suffering through finding floating promises in your code may be one way to learn about async / await in JavaScript, it's probably not a great use of your time, and setting up a quick lint rule can save you time, frustration, and maybe a broken keyboard or two.
More Reading
- Interested in learning more about promises? You may enjoy my series on
Promise.allSettled()
:
Note: The cover image for this post is based on a photo by Praveen Thirumurugan on Unsplash
Top comments (0)