0

My actual use case is batching a bunch of network calls (input: an array of args objects), and packaging the results (output: a corresponding array of specific success/failure replies for each request). My goal is to not have to pollute the rest of my code with async/await when calling this batching function. I want the batching call (here, represented by MySyncFunction) to be called synchronously (as it is in TestCaller), even though internally it uses asynchrony to offload IO costs. In a nutshell, I just want MySyncFunction to resolve all promises before returning, and not continue in its parent.

What I provide below is a minimally reproducible example of the logic of my problem.

function sleep(ms : number): void
{
    return new Promise(resolve => setTimeout(resolve, ms));
}

//notice: no async here
function MySyncFunction(): string[]
{
    const result : string[] = [];

    async function MyAsync_BeginEnd(): Promise<void>
    {
        console.log("MyAsync_Begin");
        result.push("Alice");
        await sleep(1000);
        console.log("MyAsync_End");
        result.push("Charlie");
    }

    async function MyAsync_Middle(): Promise<void>
    {
        await sleep(100);
        console.log("MyAsync_Middle");
        result.push("Bob");
    }

    async function MyAsyncFunction(): Promise<void>
    {
        const promise1 = MyAsync_BeginEnd();
        const promise2 = MyAsync_Middle();
        await promise1;
        await promise2;
    }

    // supposed to resolve everything before moving on
    (async () => await MyAsyncFunction())();

    return result;
}

function TestCaller():void
{
    console.log("Before MySyncFunction");
    const strarr: string[] = MySyncFunction();
    console.log("After  MySyncFunction; ", strarr);
}

TestCaller()

You can run it (once saved in a file called test_async_await.ts) with tsc --target es6 --lib es2015,dom test_async_await.ts && node test_async_await.js

With the preceding code, which does compile and run, I am expecting the logs to read:

Before MySyncFunction
MyAsync_Begin
MyAsync_Middle
MyAsync_End
After  MySyncFunction;  [ 'Alice', 'Bob', 'Charlie' ]

since MySyncFunction is synchronous.

However, they instead read:

Before MySyncFunction
MyAsync_Begin
After  MySyncFunction;  [ 'Alice' ]
MyAsync_Middle
MyAsync_End

as if MySyncFunction was declared to be asynchronous !

Questions:

  • is this behavior intentional ? If so, why ? Is it maybe a bug to report ? I'm told asynchrony in Rust (for example) isn't like that, and works as I'd expect.

  • how do I effectively resolve the promises internally so that MySyncFunction can be called synchronously, without needing await or then ?

I have looked at the following:

asynchronous function within synchronous function in Javascript

How to return the response from an asynchronous call

Using "await" inside non-async function

But they do not solve my problem.

EDIT:

FYI, the following does not work either.

    async function MyAsyncFunction(): Promise<void>
    {
        const promise1 = MyAsync_BeginEnd();
        const promise2 = MyAsync_Middle();
        // await promise1;
        // await promise2;
        Promise.all([promise1, promise2]);
    }
Tristan Duquesne
  • 512
  • 1
  • 6
  • 16
  • This looks like a good use case for `Promise.all()` - you pass it an array of promises an when they're all resolved, you can handle the values. But you simply cannot block execution of your JS code until all promises are resolved to make an async function synchronous - it cannot be done. – rook218 Feb 21 '22 at 13:08
  • Then this looks like terrible design, honestly... :( The fact that a function not declared async behaves asynchronously is bonkers to me. The problem with `Promise.all` is that underlying all of this, I have a C NAPI addon with a limited socket pool, and I need to relaunch certain calls in the specific case that the "all sockets in use" error was returned, and `Promise.all` does not allow this to be done incrementally (at least, not well). I will heed your advice and try with `Promise.all` again, though. – Tristan Duquesne Feb 21 '22 at 13:14
  • I think it's not really possible because JS is single threaded, so it will have to complete your first call before moving to the promises. The only way I see doing this with js is just make you function async and wait for it until it completes you promises. – Peterrabbit Feb 21 '22 at 13:17
  • And I don't don't think Promise.all will help you either. Maybe you could just try to make a purely synchronous implementation as you have to wait for everything ayway. – Peterrabbit Feb 21 '22 at 13:19
  • Yep, just declare this function async. It's not terrible design on JS's part, it's just that JS is single threaded and has a simple event loop, so there's just no way for you to step in and say "only check the event loop when the event loop is empty." If you design your methods to complement JS's design, this should be pretty simple. Declare your method async, then just handle the results the way you want when they come back with a `.then()` chain. – rook218 Feb 21 '22 at 13:28
  • Peterrabbit: I do have to wait for the whole of it to be done, but clearly SIMD design improves on performance by a mile (as proven by the other bindings over our C, including the aforementioned Rust), so parallelizing batches of similar calls is a must. rook218: this basically entails making 80% of our web codebase's functions async, which is really, really not desirable (clarity, scale of refactor), when asynchrony can, and should, be isolated in this case (at least, if it was better designed/implemented in JS). – Tristan Duquesne Feb 21 '22 at 13:35
  • That's definitely very interesting... I have not seen or heard of any kind of web call in JS being synchronous before. In all frameworks (React, Angular, Vue) and in the JS fetch API itself, all API calls are handled async either through a promise, an observable, a subject, etc. It would be worth looking at the rest of your code base to see how they handle making sync web calls - that may have your answer. – rook218 Feb 21 '22 at 13:43
  • @TristanDuquesne it's not bad design, it's a different approach. If you are working with an older codebase, it may be a lot of work to transform it into modern JavaScript (e.g. async/await). That is not because of JavaScript having a bad design, but because a lot of JavaScript was written while the language was immature. If you want to understand why JavaScript works like this, and what advantages it brings, watch this fantastic presentation: https://www.youtube.com/watch?v=8aGhZQkoFbQ – Joaquim d'Souza Feb 21 '22 at 14:10

0 Answers0