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:
1// Functions which use await to execute code must be declared with the "async" keyword2async function foo() {3return await bar();4}56// written another way7const foo = async () => {8await bar();9};
The async keyword is used to adorn the parent function, to let JavaScript know that somewhere inside the function you're going to be awaiting 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:
1async function writeToDb() {2// asynchronously write to DB3}4writeToDb(); // <- 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:
1async function createNewRecordInDb(input) {2// asynchronously create new record in DB;3let newRecord = await blah(input.name, input.email);45return newRecord;6}78const entry = createNewRecordInDb({9name: 'John Doe',10email: 'foo@bar.com'11);1213console.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:
1{2"plugins": ["no-floating-promise"]3}
and then adding the rule to your rules object:
1{2"rules": {3"no-floating-promise/no-floating-promise": 24}5}
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!
1{2/* ... */3"rules": {4"@typescript-eslint/no-floating-promises": "error"5}6}
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