10

I have multiple API calls to be made which fetch via an API, write data to DB via API, send output to front end via another API.

I have written async function with await like below.

The first two should run one after another but the third one can run independently and doesn't need to wait for the first two fetch statements to complete.

let getToken= await fetch(url_for_getToken);
let getTokenData = await getToken.json();

let writeToDB = await fetch(url_for_writeToDB);
let writeToDBData = await writeToDB.json();

let frontEnd = await fetch(url_for_frontEnd);
let frontEndData = await frontEnd.json();

What is the best way to handle such multiple fetch statements ?

starball
  • 20,030
  • 7
  • 43
  • 238
Yasar Abdullah
  • 181
  • 2
  • 2
  • 4
  • 6
    You could have a look at [Promise.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all). – Yannick K Nov 12 '19 at 09:18
  • 2
    @YannickK Would Promise.all be necessary here? Couldn't he just use .then() instead? He's not waiting for the completion of both, but rather the first **then** second, then third irrespective of those two. – Kobe Nov 12 '19 at 09:28
  • 1
    @Kobe I think the main issue in this case is that OP wants to separate the server and client calls since they're not _dependent_ on each other - and it would be silly performance-wise if they waited on each other - but if _any_ of them fail you want a rejection. You're definitely right that he could do without `Promise.all`, but in this case I'd imagine it would be cleaner (and easier to build on in the future) if he wrapped everything in one `Promise.all` call, specifically for error handling. – Yannick K Nov 12 '19 at 09:57
  • @Kobe Because [`Promise.all` is essential for proper error handling](https://stackoverflow.com/q/46889290/1048572) and waiting for the completion of both the first-then-second and the third promises. – Bergi Nov 12 '19 at 10:44
  • 1
    The [simplest answer](https://stackoverflow.com/a/58815663/199263) solves the problem best, but unfortunately was undeservedly downvoted. It’s worth giving it a shot, @Yasar Abdulllah. – AndreasPizsa Nov 12 '19 at 11:56
  • @YasarAbdullah You can accept one answer (if it helps you) by click on big gray check button on its left side. If you wish you can add +10 points to any author of any good answer by click upper gray triangle – Kamil Kiełczewski Nov 13 '19 at 21:59

4 Answers4

2

It's easier if you work with promise "creators" (= function that return promises) rather than raw promises. First, define:

const fetchJson = (url, opts) => () => fetch(url, opts).then(r => r.json())

which returns such a "creator". Now, here are two utilities for serial and parallel chaining, which accept both raw promises and "creators":

const call = f => typeof f === 'function' ? f() : f;

const parallel = (...fns)  => Promise.all(fns.map(call));

async function series(...fns) {
    let res = [];

    for (let f of fns)
        res.push(await call(f));

    return res;
}

Then, the main code can be written like this:

let [[getTokenData, writeToDBData], frontEndData] = await parallel(
    series(
        fetchJson(url_for_getToken),
        fetchJson(url_for_writeToDB),
    ),
    fetchJson(url_for_frontEnd),
)

If you don't like the dedicated "creator" wrapper, you can define fetchJson normally

const fetchJson = (url, opts) => fetch(url, opts).then(r => r.json())

and use inline continuations right where series or parallel are called:

let [[getTokenData, writeToDBData], frontEndData] = await parallel(
    series(
        () => fetchJson('getToken'),
        () => fetchJson('writeToDB'),
    ),
    () => fetchJson('frontEnd'), // continuation not necessary, but looks nicer
)

To bring the idea further, we can make series and parallel return "creators" as well rather than promises. This way, we can build arbitrary nested "circuits" of serial and parallel promises and get the results in order. Complete working example:

const call = f => typeof f === 'function' ? f() : f;

const parallel = (...fns)  => () => Promise.all(fns.map(call));

const series = (...fns) => async () => {
    let res = [];

    for (let f of fns)
        res.push(await call(f));

    return res;
};

//

const request = (x, time) => () => new Promise(resolve => {
    console.log('start', x);
    setTimeout(() => {
        console.log('end', x)
        resolve(x)
    }, time)
});

async function main() {
    let chain = series(
        parallel(
            series(
                request('A1', 500),
                request('A2', 200),
            ),
            series(
                request('B1', 900),
                request('B2', 400),
                request('B3', 400),
            ),
        ),
        parallel(
            request('C1', 800),
            series(
                request('C2', 100),
                request('C3', 100),
            )
        ),
    );

    let results = await chain();

    console.log(JSON.stringify(results))
}

main()
.as-console-wrapper { max-height: 100% !important; top: 0; }
georg
  • 211,518
  • 52
  • 313
  • 390
  • why don't you just return promise from `fetchJson`? I can't see any benefits from wrapping a promise with another function. And usefulness of `series` is questionable since the next function in a series typically requires the return value of previous function(s). – marzelin Nov 12 '19 at 11:55
  • 1
    ah, yes. All this ceremony just to use `series`. I'd rather prefer async iife for sequences and `Promise.allSettled` instead of `parallel`. – marzelin Nov 12 '19 at 12:45
  • @georg your solutions looks complicated - can you expaliain what are the advantages of your approach? – Kamil Kiełczewski Nov 13 '19 at 19:47
  • @georg - ok, but can you answer my question - what are the advantages of your generic techniques used in this answer? Usually more generic code need some documentation about idea hidden behind and advantages/features provide by it (e.g like frameworks). – Kamil Kiełczewski Nov 13 '19 at 21:56
2

There are many ways but the most universal is to wrap each async code path in an async function. This gives you flexibility of mix & matching async return values as you please. In your example you can even inline code with async iife's:

await Promise.all([
  (async() => {
    let getToken = await fetch(url_for_getToken);
    let getTokenData = await getToken.json();

    let writeToDB = await fetch(url_for_writeToDB);
    let writeToDBData = await writeToDB.json();
  })(),
  (async() => {
    let frontEnd = await fetch(url_for_frontEnd);
    let frontEndData = await frontEnd.json();
  })()
]);
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
marzelin
  • 10,790
  • 2
  • 30
  • 49
1

You can use .then(), rather than await:

fetch(url_for_getToken)
  .then(getToken => getToken.json())
  .then(async getTokenData => {
    let writeToDB = await fetch(url_for_writeToDB);
    let writeToDBData = await writeToDB.json();
    // Carry on
  })

fetch(url_for_frontEnd)
  .then(frontEnd => frontEnd.json())
  .then(frontEndData => {
    // Carry on  
  })
Kobe
  • 6,226
  • 1
  • 14
  • 35
  • 1
    Don't forget to handle errors on those promise chains - or `Promise.all` them and return them to the caller. – Bergi Nov 12 '19 at 09:47
-1

Run independent request (writeToDB) at the beginning and without await

let writeToDB = fetch(url_for_writeToDB);

let getToken = await fetch(url_for_getToken);
let getTokenData = await getToken.json();

// ...
Kamil Kiełczewski
  • 85,173
  • 29
  • 368
  • 345
  • 1
    @Bergi the question is not about error handling - error handling (like try-catch) is important but it is separate topic – Kamil Kiełczewski Nov 12 '19 at 10:03
  • `await` has error-handling built in - when the promise rejects, you get an exception, the promise returned by the `async function` call will reject, and the caller will notice. If you suggest to drop the `await` keyword and run the promise chain independently, you must not do so without thinking about error handling. An answer that doesn't at least mention it (or preserves the original behaviour of the caller getting a rejection) is a bad answer. – Bergi Nov 12 '19 at 10:41
  • 1
    This answer solves OP’s problem and it‘s also the simplest of all presented answers so far. I’m curious as to why this was downvoted? The OP stated "_the 3rd one can run independently and no need to wait for the first 2 fetch statements to complete._", so executing it first is just fine. – AndreasPizsa Nov 12 '19 at 11:52
  • @AndreasPizsa [It's too simple](https://stackoverflow.com/a/32385430/1048572). I wouldn't have downvoted it if there was at least a disclaimer that errors are unhandled - as soon as it is mentioned, anyone can make their own opinion about whether or not it is appropriate for their case - it even might be for the OP. But ignoring this leads to the worst bugs of all, and makes this answer not useful. – Bergi Nov 12 '19 at 22:56
  • 1
    Thanks for commenting @Bergi. Given that OP’s question didn’t include error handling either, I would assume it’s being left out for brevity and beyond the scope of the question. But yes, anyone can vote with their own opinion. – AndreasPizsa Nov 13 '19 at 07:04
  • 1
    @AndreasPizsa As I said, `await` *does* handle promise errors by throwing an exception, which could get caught somewhere (in the code not shown). But this answer changes the error handling behaviour of the code without mentioning it, and that's dangerous. – Bergi Nov 13 '19 at 10:34