0

I have the following 2 functions, each returns a Promise:

const getToken = () => {
  return new Promise((resolve, reject) => {
    fs.readFile('token.txt', (err, data) => {
      if (err) { return reject(err) }
      if (!tokenIsExpired(data.token)) {
        return resolve(data.token)
      } else {
        return requestNewToken()
      }
    })
  })
}

const requestNewToken = () => {
  return new Promise((resolve, reject) => {
    restClient.get(url, (data, res) => {
      fs.writeFile('tokenfile.txt', data.token, err => {
        resolve(data.token)
      })
    })
  })
}

function1()
  .then(value => {
    console.log('taco')
  })
  .catch(err => {
    console.log(err)
  })

So function1 runs, and (depending on some condition), it sometimes returns function2, which is returning another Promise. In this code, when function2 is called, the console.log('taco') never runs. Why is this? I thought that if you return a Promise from within a Promise, the resolved value of the nested Promise is what is resolved at the top level.

In order for me to get this to work, I have to do this:

const getToken = () => {
  return new Promise((resolve, reject) => {
    if (!tokenIsExpired()) {
      return resolve(getToken())
    } else {
      return requestNewToken ()
        .then(value => {
          resolve(value)
        })
    }
  })
}

That works, but it seems like I'm doing something wrong. It seems like there should be a more elegant way to handle/structure this.

Jake Wilson
  • 88,616
  • 93
  • 252
  • 370
  • is it nesessary for function 2 to return a promise? – Poku May 18 '17 at 17:42
  • Or can you write `const function1 = () => { if (someCondition) { return Promise.resolve('foobar') } else { return function2() } }`? – ephemient May 18 '17 at 17:46
  • Why are you using promises at all? Your code is synchronous. If not, please [edit] your question to show us your actual code. – Bergi May 18 '17 at 17:47
  • 1
    "*That works*" - actually it doesn't for rejections. "*it seems like I'm doing something wrong*" - Yes indeed, that's the [`Promise` constructor antipattern](http://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! – Bergi May 18 '17 at 17:50
  • I agree with @Bergi. As the code is posted here you should just go with a synchronous solution otherwise please elaborate on what you are trying to do. – Poku May 18 '17 at 18:10
  • I'm not showing all my code because I'm trying to simplify it for people to read. But yes, they need to return Promises (or callbacks). In reality, function1 uses `fs.exists` and `fs.readFile` and function2 connects to a REST API and pulls some values and then uses `fs.writeFile`. Lots of asynchronous things happening in both functions. It is not synchronous. I have added some comments to my code to indicate that yes, asynchronous things happen. – Jake Wilson May 18 '17 at 18:43
  • Thanks @Bergi perhaps you could help me rewrite this properly? I don't understand the antipattern issue here. – Jake Wilson May 18 '17 at 18:46
  • @JakeWilson Maybe you should ask a separate question for that, with the whole unsimplified code, otherwise I can't really rewrite it. – Bergi May 18 '17 at 18:55
  • The unsimplified code is literally 100 lines of code. You think that would actually help? Seems like that would just confuse people. Seems a lot more simple to just say "// async code here"... – Jake Wilson May 18 '17 at 18:58
  • I have added more explicit code to show the actual async functions being called. Any help would be appreciated beyond just saying "you're doing it's wrong". Thanks, – Jake Wilson May 18 '17 at 19:09
  • @Bergi I've been reading about the Antipattern. Its saying I should return the Promise as opposed to using .then() within the first function. I am returning the Promise in the first example but it's not working. Can you explain the problem? – Jake Wilson May 18 '17 at 19:26
  • It says you should *chain* a `.then()` *onto* the `new Promise` (if that is necessary at all), and `return` from the `then`callback function. You should *never* use other promises, or even `then`, within the `new Promise` callback function. – Bergi May 18 '17 at 20:59
  • Ah that is finally making sense. Thank you for your patience sir! Feel free to leave a proper answer below for credit. – Jake Wilson May 18 '17 at 22:32

1 Answers1

1

You're right that promises auto-unwrap, but in this case you're returning from inside a promise constructor, which is ignored, you need to invoke either resolve or reject instead of using return. I think this might be the solution you're looking for:

const function1 = () => {
  return new Promise((resolve, reject) => {
    if (someCondition) {
      resolve('foobar')
    } else {
      resolve(function2());
    }
  })
}

Inside a promise constructor, you need to call resolve or reject, which are equivalent to using return or throw from inside a then callback.

If you find this distinction confusing (I do), you should avoid the promise constructor entirely by just beginning a new chain with Promise.resolve, like this:

const function1 = () => {
  return Promise.resolve().then(() => {
    if (someCondition) {
      return 'foobar';
    } else {
      return function2();
    }
  })
}

const function2 = () => {
  return new Promise((resolve, reject) => {
    resolve('hello world')
  })
}
someCondition = false;
function1()
  .then(value => {
    console.log(value)
  })

With your updated code, I recommend using a library to wrap APIs, rather than accessing the promise constructor yourself. For example, using bluebird's promisify:

const bluebird = require('bluebird');
const readFile = bluebird.promisify(fs.readFile);
const writeFile = bluebird.promisify(fs.writeFile);
const getUrl = bluebird.promisify(restClient.get, {multiArgs:true});

const getToken = () => {
  return readFile('token.txt')
    .then((data) => {
      if(!tokenIsExpired(data.token)) {
        return data.token;
      } else {
        return requestNewToken();
      }
    });
};

const requestNewToken = () => {
    return getUrl(url)
      .then(([data, res]) => {
        return writeFile('tokenFile.txt', data.token)
          .then(() => data.token);
      });
};

I've remained faithful to your source code, but I'll note there may be a bug to do with writing data.token, and later trying to read the token property in that file.

Why use a library instead of the Promise constructor?

  • It allows you to write code which deals only with promises, which is (hopefully) easier to understand
  • You can be confident that callback APIs are correctly converted without losing errors. For example, if your tokenIsExpired function throws an error, using your promise constructor code, it would be lost. You would need to wrap all of your inner callback code in try {} catch(e) {reject(e)}, which is a hassle and easily forgotten.
Frances McMullin
  • 5,516
  • 2
  • 18
  • 19
  • 1
    s/can/should :-) – Bergi May 18 '17 at 17:48
  • these examples does not trigger the promise from function2 – Poku May 18 '17 at 17:51
  • @Poku What do you mean by "trigger"? – Bergi May 18 '17 at 18:01
  • @Bergi that this code will not print out Helle world to the console. – Poku May 18 '17 at 18:08
  • @Poku `function1().then(console.log)` works just fine for me – Bergi May 18 '17 at 18:19
  • Is it acceptable to resolve a Promise? `resolve(new Promise(){...})`?? I have never seen that before. – Jake Wilson May 18 '17 at 18:45
  • Yep, it's totally fine. Here's the relevant section in the original spec https://promisesaplus.com/#point-49 Essentially the promise under construction adopts the state of the inner promise. – Frances McMullin May 18 '17 at 18:56
  • 1
    @JakeWilson Yes, it's acceptable - that's basically what also happens when you `return` a promise from a `then` callback. But in general `resolve(promise)` should never be done, as creating promises inside the `Promise` constructor is [said antipattern](http://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it). – Bergi May 18 '17 at 18:57