Warm tip: This article is reproduced from serverfault.com, please click

What Is the Proper Way to define resolve/reject in async applications

发布于 2020-11-28 01:49:15

I am trying to build a financial system in NodeJS. I am refactoring my code from using callbacks to using Promises to minimize nesting. But I am struggling with understanding what would the proper meaning of a Resolve or a Rejection should be. Let's say in a situation where I am checking the balance before debiting an account, would the result where the balance is insufficient be a resolve or a reject? Does Resolve/Reject pertain to success or failure of code execution or that of the logical process?

Questioner
CliffTheCoder
Viewed
0
jfriend00 2020-11-28 17:40:00

There is lots of design flexibility for what you decide is a resolved promise and what is a rejected promise so there is no precise answer to that for all situations. It often takes some code design judgement to decide what best fits your situataion. Here are some thoughts:

  1. If there's a true error and any sequence of operations of which this is one should be stopped, then it's clear you want a rejection. For example, if you're making a database query and can't even connect to the database, you would clearly reject.

  2. If there's no actual error, but a query doesn't find a match, then that would typically not be a rejection, but rather a null or empty sort of resolve. The query found an empty set of results.

  3. Now, to your question about debiting an account. I would think this would generally be an error. For concurrency reasons, you shouldn't have separate functions that check the balance and then separately debit it. You need an atomic database operation that attempts to debit and will succeed or fail based on the current balance. So, if the operation is to debit the account by X amount and that does not succeed, I would think you would probably reject. The reason I'd select that design option is that if debiting the account is one of several operations that are part of some larger operation, then you want a chain of operations to be stopped when the debit fails. If you don't reject, then everyone who calls this asynchronous debit function will have to have separate "did it really succeed" test code which doesn't seem proper here. It should reject with a specific error that allows the caller to see exactly why it rejected.

Let's say in a situation where I am checking the balance before debiting an account, would the result where the balance is insufficient be a resolve or a reject?

There's room to go either way, but as I said above, I'd probably make it a reject because if this was part of a sequence of operations (debit account, then place order, send copy of order to user), I'd want the code flow to take a different path if this failed and that's easiest to code with a rejection.

In general, you want the successful path through your code to be able to be coded like this (if possible):

try {
     await op1();
     await op2();
     await opt3();
} catch(e) {
    // handle errors here
}

And, not something like:

try {
     let val = await op1();
     if (val === "success") {
         await op2();
         ....
     } else {
         // some other error here
     }
 } catch(e) {
    // handle some errors here
 }

Does Resolve/Reject pertain to success or failure of code execution or that of the logical process?

There is no black and white here. Ultimately, it's success or failure of the intended operation of the function and interpreting that is up to you. For example, fetch() in the browser resolves if any response is received from the server, even if it is a 404 or 500. The designers of fetch() decided that it would only reject if the underlying networking operation failed to reach the server and they would leave the interpretation of individual status codes to the calling application. But, often times, our applications want 4xx and 5xx statuses to be rejections (an unsuccessful operation). So, I sometimes use a wrapper around fetch that converts those statuses to rejections. So, hopefully you can see, it's really up to the designer of the code to decide which is most useful.

If you had a function called debitAccount(), it would make sense to me that it would be considered to have failed if there are insufficient funds in the account and thus that function would reject. But, if you had a function called checkBalance() that checks to see if a particular balance is present or not, then that would resolve true or false and only actual database errors would be rejections.