15

Why does calling the second function .then(notPromise) still pass the argument to the third function .then(promiseC) even though notPromise() is just a regular function?

I thought only promises can be used with .then() but somehow it still executes (and passes the arguments) properly.

promiseA()
    .then(notPromise)
    .then(promiseC);


function promiseA() {
    return new Promise(function (resolve, reject) {
        const string = "a";
        resolve(string);
    });
}


function notPromise(string) {
    const nextString = "b"
    const finalString = string + nextString;

    return finalString;
}


function promiseC(string) {
    return new Promise(function (resolve, reject) {
        const nextString = "c";

        const finalString = string + nextString;

        alert(finalString);
        resolve(finalString);
    });
}
jjrabbit
  • 353
  • 1
  • 2
  • 13
  • 7
    Any value returned from a function passed to `.then()` is implicitly wrapped with `Promise.resolve()` – Patrick Roberts Feb 14 '19 at 18:58
  • 2
    You might say this is because [Promises are broken](https://medium.com/@avaq/broken-promises-2ae92780f33), but this is simply how `.then` is specified: if the value that's returned looks like a promise, return that promise, otherwise return a Promise that will resolve with that value. – Scott Sauyet Feb 14 '19 at 18:58
  • https://stackoverflow.com/questions/28993673/when-to-make-a-function-deferred-using-promises – Bergi Feb 14 '19 at 20:45

3 Answers3

16

The then() method returns a Promise. See docs.

A promise has a handler method. Once a Promise is fulfilled or rejected, the respective handler function will be called asynchronously. The behavior of the handler function follows a specific set of rules as stated here.

Let's go over them one by one. Here is the code we will inspect side by side. Its nothing special, just a chain of promises returning values.

let sequence = new Promise(function (resolve) {
  console.log('Say 1')
  resolve(1)
})

sequence
  .then(() => {
    console.log('Say 2')
    return 2
  })
  .then(() => {
    console.log('Say 3')
  })
  .then(() => {
    console.log('Say 4')
    return Promise.resolve(4)
  })
  .then(() => {
    return new Promise(function (resolve) {
      console.log('Say 5')
      setTimeout(() => { resolve(5) }, 1000)
    })
  })
  1. returns a value, the promise returned by then gets resolved with the returned value as its value;

In code, this is Say 2, and your original question. When a value is returned, then() returns a Promise which is resolved with the value your returned.

  1. doesn't return anything, the promise returned by then gets resolved with an undefined value;

same as above.

  1. throws an error, the promise returned by then gets rejected with the thrown error as its value;

same as above, except now then() returns a Promise which is rejected with your error.

  1. returns an already resolved promise, the promise returned by then gets resolved with that promise's value as its value;

In code this is Say 4, where the promise has already been resolved. So now then() returns a Promise which is resolved with the value 4.

  1. returns an already rejected promise, the promise returned by then gets rejected with that promise's value as its value;

same as above, except it now rejects.

  1. returns another pending promise object, the resolution/rejection of the promise returned by then will be subsequent to the resolution/rejection of the promise returned by the handler. Also, the value of the promise returned by then will be the same as the value of the promise returned by the handler.

In code, this is Say 5. If you return a promise which has not been resolved yet, then() will return a Promise with the results of your promise i.e. 5.

One thing to note, that I also actually learned recently (suggested by @Bergi in comments) was that the then() method always constructs and returns a new Promise before the chain callbacks have even started to execute. The callbacks that you pass to then() simply tells the promise the value/error that the promise should resolve/reject with.

In summary, that is why then() chaining works even when you don't specifically return a new Promise - because then() method always constructs a new promise behind the scenes and rejects/resolves that promise with the value you returned. The most complex case in above scenarios is when you return a Promise in your callback, in which case your callback promise's results are passed to the then() promise.

Rash
  • 7,677
  • 1
  • 53
  • 74
  • Okay I didn't realize a new promise gets created behind the scenes. Thanks! – jjrabbit Feb 14 '19 at 19:22
  • 3
    "*If you return another promise inside then, that promise is returned.*" - no, that's not how `then` works. That cannot work at all. It *always* creates and returns a new promise – Bergi Feb 14 '19 at 20:37
  • Thanks for pointing it out. For my knowledge, if it always returns a new Promise, how does the original promise gets evaluated/executed? I will update my answer once I understand this concept. – Rash Feb 14 '19 at 21:05
  • @Bergi I think I understood your comment now and updated the answer. Thanks for pointing out :) – Rash Feb 15 '19 at 02:02
  • 1
    @Rash The point is that the new promise gets constructed and returned before the callback even runs. Then later, when the original promise fulfills, the callback gets called and the promise that had been returned will be resolved with the result. – Bergi Feb 15 '19 at 09:51
  • @Rash If implicitly creating a promise based on return value/throwing an exception is possible like this when using `then`, why isn't this same approach used when creating promises too? What's the use of being able to explicitly define if/when the resolve/reject functions are called? I suppose this way you're not forced to throw exceptions... but that seems like a messy, half way measure. – Jake1234 Jul 18 '22 at 14:50
  • @Jake1234 I did not understand your question, you will have to be more specific. Maybe a code example would help me. Did you mean why do we need multiple `then()` instead of just doing all code logic when we first defined the promise? – Rash Jul 18 '22 at 15:48
  • @Rash Sorry, I'm being very confusing here... and my question is not completely related to the original question. It seems to me like `then` and `Promise` follow very different, inconsistent conventions, and I don't see why: Since `then((result) => { return 1})` is valid code, why not also allow `new Promise(() => { return 1})`, not requiring the resolve/reject parameters. Or alternatively, why not require a similar signature for `then` as for promises? I.e. require `.then((previousResult, resolve, reject) => {...})`. – Jake1234 Jul 18 '22 at 18:20
  • @Jake1234 This needs some detailed explanation. I wish you had asked a separate question because there isn't enough space here to write. I will give you a short version. `Why promise needs resolve/reject?` Because that makes the function async. Unlike sequential fn you now can signal the finish by calling resolve(). `Why then() does not have resolve?` Because it is a normal function that returns a promise. Then is called on a promise, so it receives only the result. To have (resolve, reject) as well it means promise would have to return result and a new promise which is not ideal. – Rash Jul 19 '22 at 11:25
1

It has to do with the promise chain, it doesn't matter if subsequent calls to then() are not promises, they are all part of the promise chain, the good thing is you can continue chaining promises, which allows you to do several async/promise operations in a row (as your example described), here is a real world example:

  // This is the generic http call
  private callServer(url: string, method: string, body?: any) {
    const uri = env.apiUrl + url;
    const session = this.sessionToken;

    const headers = {
      'Content-Type': 'application/json',
      'credentials': 'same-origin',
      'x-auth-token': session,
    };

    return fetch(uri, {
      method,
      headers,
      body: JSON.stringify(body),
    })
      .then(this.wait) // this is a timer, returns a promise
      .then(this.checkStatus) // this is a sync call
      .then((r) => r.text()) // r.text() is async
      .then((tx) => tx ? JSON.parse(tx) : {}); // this is sync
  }

You can read more about the promise chain here

Oscar Franco
  • 5,691
  • 5
  • 34
  • 56
0

TL;DR

then() always returns synchronously a promise px (to allow chaining), but if its first argument is a function fx() not returning a promise, the JS runtime will resolve (fulfill) px as soon as the previous promise in the chain is resolved (fulfilled), and treat the return value of fx() like the (function) argument of the first argument of the next then().

For example, the function:

function startAsyncStuff1() {
    console.log("startAsyncStuff1 - enter")
    fetch("http://date.jsontest.com")
        .then((response) => response.json())
        .then((data) => console.log("startAsyncStuff1 - result:", data))
}

where the first then()'s argument returns a promise, logs:

startAsyncStuff1 - enter 
startAsyncStuff1 - result: Object { date: "03-26-2023", milliseconds_since_epoch: 1679845802541, time: "03:50:02 PM" }

while the function:

function startAsyncStuff2() {
    console.log("startAsyncStuff2 - enter")
    fetch("http://date.jsontest.com")
        .then((response) => response.json())
        .then(() => "log this data instead of the json")
        .then((data) => console.log("startAsyncStuff2 - result:", data))
}

where the second then()'s argument returns a string, logs:

startAsyncStuff2 - enter
startAsyncStuff2 - result: log this data instead of the json

because the returned string overrides the data passed on by the promise returned by response.json() when it was fulfilled.

PJ_Finnegan
  • 1,981
  • 1
  • 20
  • 17