How not to forget using await everywhere in Javascript?
Asked Answered
H

1

16

Trying to write a little chrome extension, which relies on the callback-heavy query functions of the chrome.* interface, I quickly landed at promises and async/await, as I needed to guarantee the order of certain operations, while trying to avoid callback hell.

However, once I introduced async/await into some functions, every function that used them also had to be turned into an async function in order to be able to await the return value. Eventually even some global constants became promises, e.g.

const DEBUG = new Promise(function(resolve){
    chrome.management.getSelf(resolve);
}).then(function(self){
    return self.installType == 'development';
});

However, now I need to write await everywhere and introducing weird bugs like if(DEBUG){...} always being executed becomes way too easy.

While it seems possible to identify the errors using ESLINT, writing await everywhere seems unnecessarily cumbersome and thus I was wondering if Javascript has some better construct that I am missing?

(Subjectively my current use of await/async seems backwards; Promises are kept as-is unless explicitly awaited, but it seems more desirable to me to have promises awaited by default in async functions and kept as bare promises only when explicitly requested.)

Hypomania answered 1/8, 2017 at 21:39 Comment(6)
Not sure if this is what you're looking for but you could always chain your Promise.prototype.then() statements i.e. new Promise(...).then(...).then(...)Forbear
Regarding the global constants, just wrap your entire module in a function that awaits a Promise.all with all asynchronously-loaded constants and only afterwards executes your main script.Glamorous
"once I introduced async/await" - I'm pretty sure you had exactly the same problem before, as your logic has always been asynchronous. Regardless whether you use callbacks, promises, or promises with await sugar.Glamorous
@PatrickBarr This is essentially a different syntax with the same problem. Only that now instead of const x = await getX(); const y = await x.getY(); dostuff(x,y); ... I'd be writing getX().then(x => Promise.all([x, x.getY()])).then(([x,y]) => {dostuff(x,y); ...}), which isn't any harder to forget doing, but if forgotten a lot harder to refactor.Hypomania
@Glamorous Yes, the problem would have come up one way or another, since I have to use an asynchronous API. All the more it seemed weird, that the solution would be a syntax that encourages me to defensively put await statements everywhere; Hence the question about proper handling.Hypomania
Great article on how to avoid, that everything up to the top has to be async/await once you start the habbit. See "Pitfall 3" (Spoiler: you can treat the result of an async-function as a promise)Obligor
G
10

For the lack of a type system that would allow to catch such mistakes easily (did you consider Typescript or Flow?), you can use Systems Hungarian Notation for your variable names. Choose a prefix of suffix like P, Promise or $ and add it to all your promise variables, similar to how asynchronous functions are often named with an Async suffix. Then only do things like

const debug = await debugPromise

where you can quickly see that if (debug) is fine but if (debugPromise) is not.


Once I introduced async/await into some functions, every function that used them also had to be turned into an async function in order to be able to await the return value. Eventually even some global constants became promises

I would not do that. Try to make as few functions asynchronous as possible. If they are not doing intrinsically asynchronous things themselves but only rely on the results of some promises, declare those results as parameters of the function. A simple example:

// Bad
async function fetchAndParse(options) {
    const response = await fetch(options);
    // do something
    return result;
}
// usage:
await fetchAndParse(options)

// Good:
function parse(response) {
    // do something
    return result;
}
// usage:
await fetch(options).then(parse) // or
parse(await fetch(options))

The same pattern can be applied for globals - either make them explicit parameters of every function, or make them parameters of a module function that contains all others as closures. Then await the global promises only once in the module, before declaring or executing anything else, and use the plain result value afterwards.

// Bad:
async function log(line) {
    if (await debugPromise)
        console.log(line);
}
async function parse(response) {
    await log("parsing")
    // do something
    return result;
}
… await parse(…) …

// Good:
(async function mymodule() {
    const debug = await debugPromise;
    function log(line) {
        if (debug)
            console.log(line);
    }
    function parse(response) {
        log("parsing")
        // do something
        return result;
    }
    … parse(…) …
}());
Glamorous answered 1/8, 2017 at 21:54 Comment(7)
Regarding the naming, an Async suffix is pretty common; this was a .NET convention but also adopted by Bluebird.Croteau
@StephenCleary Thanks, I was already wondering whether I should clarify that one would not use the same suffix for promise variables and for asynchronous functions.Glamorous
Thank you Bergi. Cleanly explained. This was nice. There are two more scenarios - 1) errors during async/await usage for promises. 2) Reject usage issues when using async/await for promises. How do you follow good practices for handling errors in async/await? Second, any good way of handling reject codes when doing async/await for promises? Any references or code examples is fineHassett
@Hassett Just use try/catch like everywhere else. Or some variations depending on what you need exactly.Glamorous
Thank you for taking your precious time to answer it.Hassett
Results of async-Functions can be treated like Promises. This makes life a lot easier…Obligor
@FrankNocke "can be treated as" hardly makes sense. They are promises :-)Glamorous

© 2022 - 2024 — McMap. All rights reserved.