-3

If one of the goals with Promises is to prevent callback hell, why does then even exist and when should it be used?

Example:

//If this
someFunction.then((retunValue) => {

});

//Can be written like so:
const returnValue = await someFunction();

Why would anyone choose to write it the first way?

When should then be used today?

John Montgomery
  • 6,739
  • 9
  • 52
  • 68
now_world
  • 940
  • 7
  • 21
  • 56
  • Because `await` is a new feature that not every browser implements. There are also cases where you don't want to block or suspend execution in this way. – zero298 Jun 29 '20 at 21:05
  • @B001ᛦ becuase multiple .then in side of other .then can look sloppy and is pretty much the same as callback hell – now_world Jun 29 '20 at 21:05
  • 1
    And, `await` actually uses `.then()` in order to do its job in some cases. – jfriend00 Jun 29 '20 at 21:06
  • Top-level `await` doesn't even exist yet. – zerkms Jun 29 '20 at 21:06
  • Callback hell means callback within callback ... (a lot of nested callbacks). `then` prevents that by chaining the callbacks in a cleaner way (**chaining** is a lot cleaner than **nesting**) – ibrahim mahrir Jun 29 '20 at 21:06
  • 1
    if async, we prefer async all the way. some projects may has only some modules uses async, it may prefer to use `.then()` instead. – Sphinx Jun 29 '20 at 21:07
  • 2
    @orangeMint You might want to have a look at [how promise `then` improves on callback hell](https://stackoverflow.com/a/22562045/1048572) by having a return value – Bergi Jun 29 '20 at 21:09
  • 1
    @jfriend00 `await` uses `then` to do its job in *all* cases – Bergi Jun 29 '20 at 21:10
  • 2
    "*…and when should it be used?*" - there are still [some cases in exception handling](https://stackoverflow.com/questions/44663864/correct-try-catch-syntax-using-async-await) where `then(onFulfilled, onRejected)` is superior to `await`+`try`/`catch`. – Bergi Jun 29 '20 at 21:12
  • @Bergi - Are you sure there are no internally optimized code paths with native promises that bypass actually calling a `.then()` method, particularly when the only listener is one `await`? – jfriend00 Jun 29 '20 at 21:16
  • The most obvious reason to use it today is when you are targeting a platform that does not support `async/await`, like IE 11 for instance. You could transpile code for those platforms using transpilers like Babel though. – Jake Holzinger Jun 29 '20 at 21:17
  • 1
    @jfriend00 No, I'm not sure about that, but I think that would digress into a discussion about when an optimisation (such as inlining) is still considered a method call and when not :-) – Bergi Jun 29 '20 at 21:32
  • @Bergi - Well that's why I didn't say it always uses `.then()`. I thought I may have a short cut path with native promises. – jfriend00 Jun 29 '20 at 22:42

3 Answers3

4

then predates async/await by some years.

Even with then you avoid callback hell because you can chain it (and use interstitial variables as part of the chaining) instead of nesting callbacks.

Quentin
  • 914,110
  • 126
  • 1,211
  • 1,335
0

await pause execution of current function until promise is resolved/rejected, while then creates new promise and execution of the current function continues.

Consider following code that has different results for await -vs- then:

const wait = (num) => new Promise((resolve, reject) => {
  setTimeout(resolve, num);
});

async function main() {
  await wait(1000);
  console.log('await: first');
  await wait(50);
  console.log('await: second');

// - VS -

  wait(1000).then(() => console.log('then: first'));
  wait(50).then(() => console.log('then: second'));
}

main();
Johnathan Barclay
  • 18,599
  • 1
  • 22
  • 35
peter
  • 583
  • 2
  • 11
  • Well this has different results only because you translated from `await` to `then` incorrectly. It needs to be `return wait(1000).then(() => { console.log('then: first'); return wait(50).then(() => { console.log('then: second'); }); });` – Bergi Jun 30 '20 at 10:00
0

The super short answer is that .then() came first and there was no reason to remove it from the language when await came along later. In fact, even now that we have await, there are still reasons when .then() and/or .catch() are more convenient or more suited for the task than await. Here are some of those reasons:

  1. It predates await and was the only way to get values out of a promise for several years.
  2. There are situations where p.then(fnResolve, fnReject) is useful.
  3. There are situations where a single p.catch() or p.then().catch() is easier and simpler than using await with try/catch around it.
  4. There are situations where you don't want the entire function to be suspended like await will do, but you still want a handler for when the promise is done.
  5. We do not yet have top level await in many environments yet so with top level promises, the only way to get the value out at the top level is to use .then() or wrap it in a non-top level function.
  6. Higher level promise control flow functions such as Promise.all(), Promise.race() and Promise.allSettled() and the many others that people have designed themselves all use .then() to monitor multiple promise chains that are in-flight at once because they don't want to force sequencing which is what await is designed for. In other words, await is designed to help you sequence things one after another. It lets you create a promise chain and "waits" for it to finish before executing the next line of code. Sometimes, that is not the desired control flow. Sometimes you want things in flight at the same time, not sequenced.

So, while await can replace .then() in many cases, they are not identical and there are still situations where .then() is useful instead of await and, of course, it existed first so it isn't about to be removed from the language.

Discussion of item #1:

Before await, the only way to get a value out of a promise was with .then() and there was no need to remove .then() when await was later added.

someAsynchronousFunc().then(function(result) {
    console.log(result);
}).catch(function(err) {
    console.log(err);
});

I even used regular functions because promises predated arrow functions too.

Discussion of item #2:

As Bergi shows here, there are times when it's easier to isolate one specific error source with the structure:

p.then(fnResolve, fnReject)

You can, certainly achieve the same result with await, but not as cleanly. I won't repeat the above referenced answer here. Please read it for details.

Discussion of item #3:

There are situations where a single p.catch() or p.then().catch() is easier and simpler than using await with try/catch around it.

I regular see this when I have a bunch of logic involving asynchronous operations and then there's some final cleanup at the end like closing files. I don't need to await that as the rest of the flow doesn't need to wait for it, I don't intend to propagate a close error, I do need to catch errors and I do need to log the error. So, I choose to do something like this:

async function someFunc() {
     let fileHandle;
     try {
         ... multiple pieces of business logic using await
     } catch(e) {
         ... handle errors in business logic
     } finally {
         if (fileHandle) {
             fileHandle.close().catch(err => {
                 console.log("Error closing file", err);
             });
         }
     }
}

I choose not to use await primarily because I don't need someFunc() to wait for the close() operation to complete and I'm not trying to propagate any error with close(), particularly if all I was doing was reading from the file. A close error is both highly unlikely (when only reading) and also of no real consequence. This could be done with await, but it would both be more code and would wait when I don't want it to wait. The single .catch() is just simpler.

Discussion of item #4:

There are situations where you don't want the entire function to be suspended like await will do, but you still want a handler for when the promise is done.

This would mostly occur when you're branching your promise chain into a separate, independent promise chain. This isn't that common a pattern, but does occasionally happen. Let's suppose that you have some middleware that collects statistics in a separate database server. You'd like to know if there are errors in recording the statistics, but it's not something you want to communicate back to the user and it's not something you want the response back to the http request to "wait" for. In this case, you want to fire off a new separate promise chain to record the statistics that you main promise chain does not wait for.

app.use("/update", (req, res, next) => {
    // update statistics in separate database
    updateStats(req).catch(err => {
        // log error
        console.log("updateStats error", req.url, err);
    });
    // continue on the request chain without waiting for stats update to finish
    next();
});

Discussion of item #5:

We do not yet have top level await in many environments yet so with top level promises, the only way to get the value out at the top level is to use .then() or wrap it in a non-top level function.

If you have a promise at the top level, then you can do this:

someAsyncFunc(...).then(result => {
    // do something with result
}).catch(err => {
    // do something with error
});

Or, you can do something like this:

(async function() {
    try {
        let result = await someAsyncFunc(...);
        // do something with result
    } catch(e) {
        // do something with error
    }
})();

There are times when the 2nd form may actually be the desirable solution, particularly if you're trying to sequence multiple asynchronous operations, all with await. But, if the solution is no more complex than what we have above, the async IIFE is clearly a contrived structure just to allow the user a single await. It's not simpler or cleaner.

Discussion of item #6:

Higher level promise control flow functions such as Promise.all(), Promise.race() and Promise.allSettled() and the many others that people have designed themselves (such as mapConcurrent() that I mentioned below) all use .then() and/or .catch() to monitor multiple promise chains that are in-flight at once.

If you're going to have multiple asynchronous operations in flight at once, you (somewhat by definition), do not want to await each individual one. Instead, you want to launch them all, use some code that monitors them all and then provide some summary information when a specific condition has been met. With Promise.all(), you want to know when the first one rejects or when all have succeeded. With Promise.race(), you want to know when the first one rejects or succeeds. With Promise.allSettled(), you want to know when they are all done (whether successfully or not). None of these are implemented with await because all the asynchronous operations are launched in parallel. await is designed to serialize asynchronous operations. It doesn't do parallel operation.

For example, if you want to fetch an array of URLs, you might do this:

Promise.all([url1, url2, url3].map(url => {
   return got(url);
})).then(arrayOfData => {
    console.log(arrayOfData);
}).catch(err => {
    console.log(err);
});

This particular code could use await, like this:

try {
    let arrayOfData = await Promise.all([url1, url2, url3].map(url => {
        return got(url);
    });
    console.log(arrayOfData);
} catch(err) {
    console.log(err);
}

But, internal to the implementation of Promise.all() or any other function that is managing N promises in flight at the same time, it doesn't want to use await because it doesn't want to serialize the operations, it wants to allow them to run in parallel and monitor them in parallel with .then() and .catch() (not serialize them with await).

Here's an example of a function named mapConcurrent() that lets you process an array of items, calling some asynchronous function on each item in the array, but rather than running the entire array in parallel, it runs N operations at a time in parallel (to conserve memory usage and in some cases to keep from overwhelming the target server or avoid rate limiting). That implementation, like others that do something similar to manage parallel asynchronous operations using promises, all use .then() and .catch() to monitor multiple operations in flight at the same time.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • I think points 2 and 3 need elaboration, and 4 is a moot point, because you don't need to immediately await a Promise. – Johnathan Barclay Jun 30 '20 at 09:02
  • @JohnathanBarclay Ìt's about `const p = doSomething().then(r => handle(r));` vs `const p = (async () { const r = await doSomething(); return handle(r); })();` – Bergi Jun 30 '20 at 10:05
  • @Bergi I'm not sure what point your trying to make? Just that `then` is more concise within a sync context? What if `p` also needs to be awaited? – Johnathan Barclay Jun 30 '20 at 10:21
  • @JohnathanBarclay Yes, that's point 4. It might not be a particularly strong one, but it's still a valid reason to use `then`. And the assumption is that `p` is not being `await`ed, but e.g. passed to `Promise.all`. – Bergi Jun 30 '20 at 10:28
  • @Bergi Ok fair enough, I'm not sure that is what point 4 is about, but it is a benefit. The `async` version could be simplified as well: `const p = (async () => handle(await doSomething()))();`, although still more verbose than `then`. – Johnathan Barclay Jun 30 '20 at 10:35
  • @JohnathanBarclay - For point 4, sometimes you may have multiple promise sequences within a function and you want them to proceed in parallel so you aren't trying to await one before executing the next. You may or may not be using `Promise.all()` to track when they are all done. Oh, I guess I could add a sixth point because things like `Promise.all()` and `Promise.race()` and `Promise.allSettled()` all use `.then()` to track multiple promise chains that are all in-flight at the same time and it would be a lot more difficult for them to do what they do by using only `await`. – jfriend00 Jun 30 '20 at 13:52
  • @JohnathanBarclay - If folks are interested, I can offer code examples of these points. The question already has an accepted answer (though a bit simplistic one) - I wasn't sure if people were interested or not. I won't be back online until much later today to work on this. – jfriend00 Jun 30 '20 at 14:08
  • @jfriend00 I think it would be useful. Comments such as _There are situations where `p.then(fnResolve, fnReject)` is useful._ don't really mean much without an example. – Johnathan Barclay Jun 30 '20 at 14:10
  • @JohnathanBarclay - I added code examples and more detailed explanation for each point. – jfriend00 Jun 30 '20 at 23:08