0

I'm writing a React application and in particular cases I have to resolve nested promises. The code works fine but I can't propagate the resolve() function up to the outer level, thus I'm not able to get the returning value.

This is the code:

  writeData(data) {
    this.store.dispatch({type: "START_LOADER"})

    return new Promise((resolve, reject) => {
      this.manager.isDeviceConnected(this.deviceId).then(res => {
        this.manager.startDeviceScan(null, null, (error, device) => {
          if (device.id === this.deviceId) {

            resolve("test") // -> this is propagate correctly

            device.connect().then((device) => {
              this.store.dispatch({type: "SET_STATUS", payload: "Device is connected!\n"})
              return device.discoverAllServicesAndCharacteristics()
            }).then((device) => {
              device.writeCharacteristicWithoutResponseForService(
                data.serviceId,
                data.charId,
                data.dataToWrite
              ).then(res => {

                resolve("test2") // -> this is not propagated

              }).catch(error => {
                reject(error.message)
              })
            }).catch((error) => {
              reject(error.message)
            });
          }
        });
      }).catch(error => {
        reject(error.message)
      })
    })
  }
...
...

  async writeAsyncData(data) {
    await this.writeData(data)
  }

When I call this function:

      this.writeAsyncData({data}).then(response => {
        // here I expect whatever parameter I have passed to resolve()
        console.log(response)
      })

In case I leave resolve("test") uncommented I can console.log it without any problem, but if I comment it, the resolve("test2") doesn't show in console.log and response is undefined.

How can I make sure that even the nested parameter of the inner resolve reach the console.log ?

ste
  • 3,087
  • 5
  • 38
  • 73
  • 4
    Avoid the [`Promise` constructor antipattern](https://stackoverflow.com/q/23803743/1048572?What-is-the-promise-construction-antipattern-and-how-to-avoid-it)! – Bergi Nov 17 '19 at 18:05
  • Are you sure that `discoverAllServicesAndCharacteristics` and `writeCharacteristicWithoutResponseForService` return promises that fulfill at some point? Did you confirm that `resolve("test2")` even gets run? – Bergi Nov 17 '19 at 18:08
  • Yes, if I add a console.log("test2") over resolve("test2") I can see the print in the log! – ste Nov 17 '19 at 18:11
  • Then I can't see any reason why it wouldn't work. Except that maybe the promise was already settled from one of the other `resolve`/`reject` calls? Especially if `startDeviceScan` calls its callback multiple times? Make sure to have a `.catch(console.error)` installed on the `writeAsyncData({data})` call. – Bergi Nov 17 '19 at 18:14
  • @Bergi, there's a nodeback in there. Is it fair to characterise promisification_not_at_the_lowest_level as the Promise constructor anti-pattern? If it's an anti-pattern does it not deserve its own name? – Roamer-1888 Nov 17 '19 at 23:36
  • @Roamer-1888 I don't know if it's fair, I've always (sub)classified this as the promise constructor antipattern since years. Do you think it needs a separate name? Feel free to coin one. (Btw, promisification-not-at-the-lowest-level can also happen if there are no promise creations inside). – Bergi Nov 18 '19 at 00:18
  • @Bergi, not really the right forum to discuss this but no it's not fair. For me, there's a big difference between `new Promise()` being unnecessary and it being in the wrong place. What do you mean by promisification-not-at-the-lowest-level also being possible if there are no promise creations inside? – Roamer-1888 Nov 18 '19 at 02:29
  • @Roamer-1888 If a `new Promise` is wrapping multiple nested asynchronous (non-promise) calls at once, or when there's too much application logic in the asynchronous callback that should go inside `then` handlers *after* `resolve`/`reject`, that's what I would call promisification-not-at-the-lowest-level. (Except we need a better, if only shorter, name :-) – Bergi Nov 18 '19 at 09:07
  • @Bergi, needs wider discussion in a better forum - suggestions? – Roamer-1888 Nov 18 '19 at 12:02

1 Answers1

5

To nest promises properly, you do NOT wrap them in yet another manually created promise. That is an anti-pattern. Instead, you return the inner promises and that will then chain them. Whatever the inner-most promise returns will be the resolved value for the whole chain.

In addition, when you have any asynchronous operations that return callbacks, you must promisify them so that you are doing all your asynchronous control flow with promises and can consistently do proper error handling also. Do not mix plain callbacks with promises. The control flow and, in particular, proper error handling gets very, very difficult. One you start with promises, make all async operations use promises.

While this code is probably simplest with async/await, I'll first show you how you properly chain all your nested promises by returning every single inner promise.

And, to simplify your nested code, it can be flattened so that rather than each level of promise making deeper indentation, you can just return the promise back to the top level and continue processing there.

To summarize these recommendations:

1. Don't wrap existing promises in another manually created promise. That's a promise anti-pattern. Besides being unnecessary, it's very easy to make mistakes with proper error handling and error propagation.

2. Promisify any plain callbacks. This lets you do all your control flow with promises which makes it a lot easier to avoid errors or tricky situations where you don't know how to propagate errors properly.

3. Return all inner promises from within the .then() handlers to properly chain them together. This allows the inner-most return value to be the resolved value of the whole promise chain. It also allows errors to properly propagate all the way up the chain.

4. Flatten the chain. If you have multiple promises chained together, flatten them so you are always returning back to the top level and not creating deeper and deeper nesting. One case where you do have to make things deeper is if you have conditionals in your promise chain (which you do not have here).

Here's your code with those recommendations applied:

// Note: I added a timeout here so it will reject
// if this.deviceId is never found
// to avoid a situation where this promise would never resolve or reject
// This would be better if startDeviceScan() could communicate back when
// it is done with the scan
findDevice(timeout = 5000) {
    return new Promise((resolve, reject) => { 
        const timer = setTimeout(() => {
             reject(new Error("findDevice hit timeout before finding match device.id"));
        }, timeout);
        this.manager.startDeviceScan(null, null, (error, device) => { 
            if (error) {
                reject(error); 
                clearTimeout(timer);
                return
            }
            if (device.id === this.deviceId) {
                resolve(device); 
                clearTimeout(timer);
            }
        });
    });
}

writeData(data) {
    this.store.dispatch({type: "START_LOADER"});
    return this.manager.isDeviceConnected(this.deviceId).then(res => {
        return this.findDevice();
    }).then(device => {
        return device.connect();
    }).then(device => {
        this.store.dispatch({type: "SET_STATUS", payload: "Device is connected!\n"})
        return device.discoverAllServicesAndCharacteristics();
    }).then(device => {
        return device.writeCharacteristicWithoutResponseForService(
            data.serviceId,
            data.charId,
            data.dataToWrite
        );
    }).then(res => {
        return "test2";  // this will be propagated
    });
}

Here's a version using async/await:

findDevice(timeout = 5000) {
    return new Promise((resolve, reject) => { 
        const timer = setTimeout(() => {
             reject(new Error("findDevice hit timeout before finding match device.id"));
        }, timeout);
        this.manager.startDeviceScan(null, null, (error, device) => { 
            if (error) {
                reject(error); 
                clearTimeout(timer);
                return
            }
            if (device.id === this.deviceId) {
                resolve(device); 
                clearTimeout(timer);
            }
        });
    });
}

async writeData(data) {        
    this.store.dispatch({type: "START_LOADER"});
    let res = await this.manager.isDeviceConnected(this.deviceId);
    let deviceA = await this.findDevice();
    let device = await deviceA.connect();
    this.store.dispatch({type: "SET_STATUS", payload: "Device is connected!\n"})
    await device.discoverAllServicesAndCharacteristics();
    let res = await device.writeCharacteristicWithoutResponseForService(
          data.serviceId,
          data.charId,
          data.dataToWrite
    );
    return "something";    // final resolved value 
}

Note: In your original code, you have two overriding definitions for device. I left that there in the first version of the code here, but changed the first one to deviceA in the second one.

Note: As your code was written, if this.manager.startDeviceScan() never finds a matching device where device.id === this.deviceId, your code will get stuck, never resolving or rejecting. This seems like a hard to find bug waiting to happen. In the absolute worst case, it should have a timeout that would reject if never found, but probably the implementation of startDeviceScan needs to communicate back when the scan is done so the outer code can reject if no matching device found.

Note: I see that you never use the resolved value from this.manager.isDeviceConnected(this.deviceId);. Why is that? Does it reject if the device is not connected. If not, this seems like a no-op (doesn't do anything useful).

Note: You call and wait for device.discoverAllServicesAndCharacteristics();, but you never use any result from it. Why is that?

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • `startDeviceScan` looks like the kind of method that would call the callback multiple times, for each found device… – Bergi Nov 17 '19 at 18:21
  • @Bergi - Hmmm, maybe so. I'll have to think about how to modify the code if that's the case. That does complicate things. It so, it doesn't seem like there's any way to know when it's done scanning. Would need to know more about that specific API. – jfriend00 Nov 17 '19 at 18:27
  • `new Promise((resolve, reject) { this.manager.startDeviceScan(null, null, (error, device) => { if (error) reject(error); else if (device.id === this.deviceId) resolve(device); }); })` probably, maybe with a timeout assuming it is continually scanning? – Bergi Nov 17 '19 at 18:33
  • @Bergi - But, if this is never satisified `device.id === this.deviceId`, then this never resolves or rejects which seems like a hard to find bug waiting to happen. That was my conundrum. I guess I can put that in for now. – jfriend00 Nov 17 '19 at 18:34
  • 1
    @Bergi - I updated the code by adding a `findDevice()` method that has similar behavior to the OP's original code, but it still subject to the issue mentioned in my previous comment. – jfriend00 Nov 17 '19 at 18:42
  • @ste - I updated my code with a `findDevice()` method that handles `startDeviceScan()` calling its callback multiple times. Please read the notes in my answer about your implementation of `startDeviceScan()`. – jfriend00 Nov 17 '19 at 18:47
  • 1
    Updated the first version to flatten the chain. – jfriend00 Nov 17 '19 at 18:56
  • Added timeout to `findDevice()` so it avoids the situation where that function could never resolve or reject if startDeviceScan never found the appropriate `device.id`. – jfriend00 Nov 17 '19 at 19:07
  • 1
    @ste - Does this work for you? For the amount I endeavored to show/teach you, I'm a bit surprised you haven't even responded. – jfriend00 Nov 17 '19 at 21:47
  • @jfriend00 I used your async/await code, much cleaner! Just couple of things: my code throws a syntax error on `reject new Error...` I had to put parenthesis, and I also removed `startDeviceScanAsync`, I can't understand how you are using it...the scan is inside findDevice, isn't it? Anyway thanks for the tips, it really solved my problem! – ste Nov 20 '19 at 13:17
  • @ste - Yes, those were both mistakes. `reject()` is a function call so it needs the parens. The old reference to `startDeviceScanAsync` was a remnant from an earlier iteration of the code. I've edited the answer to fix. Glad you figured out how to work through them. – jfriend00 Nov 20 '19 at 13:59