Skip to content

Instantly share code, notes, and snippets.

@joeytwiddle
Last active August 8, 2023 09:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joeytwiddle/7027f75a413a2eae3561858c6f7de50d to your computer and use it in GitHub Desktop.
Save joeytwiddle/7027f75a413a2eae3561858c6f7de50d to your computer and use it in GitHub Desktop.
How do you write a top-level async function?
/*
* TLDR: I want to use await in a function, but I don't want my function to return a Promise.
*
* Solution: Use this inside the body of your function: Promise.resolve(async () => { ... }).catch(...);
*
* Original post follows. (I say original, but it has slowly grown longer and longer...!)
*
* ------------------
*
* I have been using async-await and loving it. But there is one thing I'm not sure how to handle.
*
* Sometimes I write a function which calls out to Promises, but it handles all the errors itself.
*
* Sounds fine. But because it is an async function, it returns a Promise, whether I want it to or not!
*
* It's a kind of dummy Promise, because it always resolves with undefined. I have already handled the actual
* result / error.
*
* So there is a Promise with no .then() or .catch().
*
* This feels like a violation of golden rule: "A Promise must either be returned or caught." This violation could
* potentially trigger warnings from linters.
*
* So ... how do you write code like this?
*
* Approach 1 provides an example. The other approaches are possible alternatives but they each suck.
*
* Do you just follow Approach 1 and abandon the golden rule?
*/
// Approach 0 - Recommended solution
async function foo () {
const result = await getSomethingSlowly();
console.log(`result:`, result);
}
foo().catch(console.error);
// After some time, this became my favourite solution.
// It's compliant and minimal, separating the concern from the function itself.
// Approach 1
async function foo () {
try {
const result = await getSomethingSlowly();
console.log(`result:`, result);
} catch (err) {
console.error(err);
}
}
// We must use the async keyword because we want to use await inside.
// But the async keyword means that a Promise will be returned.
// The returned Promise is actually useless, so there is really no point in handling it.
// But I feel uncomfortable not handling it. Technically it is a violation of the Golden Rule.
// And it could trigger some static analyzers to complain. (See WebStorm's linter complaining in the comments below.)
// Approach 2
function foo () {
Promise.resolve().then(async () => {
const result = await getSomethingSlowly();
console.log(`result:`, result);
}).catch(err => {
console.error(err);
});
}
// Feels non-idiomatic because we are back to using .catch()
// and also the Promise keyword
// But in the end, this is the solution I have settled on.
// Approach 3
function foo () {
(async function () {
const result = await getSomethingSlowly();
console.log(`result:`, result);
}()).catch(err => {
console.error(err);
});
}
// Well we got rid of the Promise.resolve() by using an IIFE, but this looks quite fugly!
// It is also quite easy to forget the `()` in my experience
// Approach 4 - Just using good old promises
function foo () {
getSomethingSlowly().then(result => {
console.log(`result:`, result);
}).catch(err => {
console.error(err);
});
}
// No promise is returned and there are no linter warnings.
// It seems traditional Promises are still good for some things that async-await is not!
// One difference (and disadvantage) here is that `getSomethingSlowly()` could throw an Error, instead of returning
// a rejected promise.
// For that reason, I often go with Approach 2 instead.
// Approach 99, included for completeness
async function foo () {
const result = await getSomethingSlowly();
console.log(`result:`, result);
}
foo();
// Sometimes it is guaranteed that a promise will never reject. (E.g. the classic `setTimeout` delay promise.)
// But if it is possible that `getSomethingSlowly()` could reject, then in Node >= 9 the code above is dangerous,
// because a rejection will crash your process.
// However in front end code, people will sometimes use code like that when rejections are exceptional, because
// (since 2017) any rejection will be automatically logged for the developer.
// Either way, code like that is still a violation of The Golden Rule, and could trigger linters.
@joeytwiddle
Copy link
Author

joeytwiddle commented Sep 27, 2017

Here is an example linter warning. Usually this warning is very helpful. It sees me calling an async function without handling the returned Promise. It doesn't care that in this case I already handled the error inside the function.

screenshot 2017-09-27 11 06 02

So far I think I have the following options:
o1. Ignore the linter warning. (Not good practice IMHO.)
o2. Silence the linter warning for that line: //noinspection JSIgnoredPromiseFromCall in WebStorm (This is actually my favourite!)
o3. Use one of the fugly solutions above.
o4. Handle the promise like I am supposed to: foo().catch(console.error) In this case, I may as well not bother catching the error inside the function. This option sucks if many places call foo().

@joeytwiddle
Copy link
Author

joeytwiddle commented Sep 27, 2017

In IRC, ljharb recommended sticking with good old promises (Approach number 4 above)

@joeytwiddle
Copy link
Author

joeytwiddle commented Oct 10, 2017

But I am quite in favour of Approach number 2 above. (Edit: See below for why I now prefer Approach 3.)

Starting a promise chain with Promise.resolve() means that any synchronous error inside getSomethingSlowly will be passed to the .catch().

With Approach number 4, a synchronous error inside getSomethingSlowly would not be caught, and would crash the process!

So in fact Approach number 2 handles errors the same way Approach number 1 did.


A synchronous error would be something inside getSomethingSlowly that went wrong before it starts its promise chain.

For example a mistake like: argument.prop when argument is undefined, or a guard like throw Error("The args you passed are invalid");

@joeytwiddle
Copy link
Author

joeytwiddle commented Mar 28, 2018

On the other hand, we are very much fighting against the tide here.

So many places are taking Approach number 1, even without a try-catch, and relying on the engine to report unhandled rejections. (Example: react-navigation)

Perhaps we should acquiesce, and adopt approach 1 for the sake of consistency with the community!

@joeytwiddle
Copy link
Author

If I only need to call one async function, and I don't need its return value (no need for .then()) then I just use good old promises (Approach 4).

Otherwise, I use Approach 3. It is functionally the same as Approach 2, but is somewhat shorter, and its unique appearance makes it obvious why we are doing it. Approach 3 is an IIAFE.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment