0

I have a conceptual question concerning async await code : this is a snippet I have in my code:

async function uploadFileBackup(fileId, versionNumber, filePath, sync) {
//...some irrelevant code
 const toReturn = new Promise((accept, reject) => {
    s3.upload(params, async function (err, data) {
      if (err) {
        reject(err)
      } else {
        //here is the relevant code
        file.backupStatus = 'success'
        await file.save()
        accept(data)
      }
    })
  })

  if (sync)
    return toReturn
//...some other irrelevant code
}

Now if the line

await file.save()

(that is happening on a promise success) fails, then my app throws a Unhandled promise rejection and this exactly is my issue.

Also if the passed accept method throws an exception, the promise will have an unhandled rejection.

I could naturally surround the code in the else statement with a try catch block, but that won't help, worse, it will mislead the developer since he will think the error comes from the promise code s3.upload, while that part will have succeeded and the accept code will be the one that failed : which has definitely different consequences :

  • a s3 upload failure means the file didn't upload
  • but a file.save() failure means only the file status in our db was not updated, but the physical file is in the s3 system

those two errors cannot be handled in the same try catch block, but catching what happens in the accept method is also not clear (about what happened behind the scene). Finally, adding try catch blocks in callback-promise bridges complexifies the code and readability

So what are the best practices in this case

Zied Hamdi
  • 2,400
  • 1
  • 25
  • 40

1 Answers1

3

You need to avoid the Promise constructor antipattern! And one must not pass an async function as a normal callback to a function that ignores the return value, and therefore doesn't handle errors in the returned promise.

You should instead promisify s3.upload alone

function upload(params) {
  return new Promise((resolve, reject) => {
    s3.upload(params, (err, data) => {
      if (err) reject(err)
      else resolve(data)
    })
  })
}

and then use it like

async function uploadFileBackup(fileId, versionNumber, filePath, sync) {
  … // some irrelevant code
  if (sync) {
    const result = await upload(params)
    file.backupStatus = 'success'
    await file.save()
    return result
  }
  … // some other irrelevant code
}

Also you should avoid creating toReturn as a promise that is only conditionally awaited (if (sync)) and otherwise forgotten about without handling errors.

If you now want to handle errors from await upload or await file.save you can wrap them in try/catch or do whatever is appropriate. If you don't handle them, they will reject the promise returned by uploadFileBackup and need to be handled by its caller somewhere, but they will not lead to unhandled rejections from stray promises.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Ok so in other words you mean, the resolve method should never do something relevant. This means I will have to repeat the logic that comes after a promise resolution, I cannot bind that logic to the call itself – Zied Hamdi Nov 09 '17 at 10:59
  • Also, this method could be called asynchroneously o avoid blocking the request, that's why it proposes the parameter sync – Zied Hamdi Nov 09 '17 at 11:06
  • 1
    Not sure what you mean by "resolve method"? Yes, the callback that you pass into `s3.upload` should only resolve the promise and do nothing else, all other things should happen in a `then` callback (or after `await`). – Bergi Nov 09 '17 at 11:15
  • What parts do you need to repeat? I don't see anything in the code in your question. What do you want to "bind to the call itself"? – Bergi Nov 09 '17 at 11:16
  • 1
    That parameter "sync" doesn't make much sense to me - the upload and file save are always asynchronous regardless what arguments you are passing (and it would indeed be a horrible practice to have the function block, even more so depending on one of its arguments). Maybe you will need to show the rest of the code (that you had marked as "irrelevant")? – Bergi Nov 09 '17 at 11:18
  • You're right, I realized that the function it self returns a Promise, so if I want to call it asynchroneously, I'd be better off calling the method with a then().catch() syntax – Zied Hamdi Nov 15 '17 at 11:22