question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Promises fail in unexpected fashion

See original GitHub issue

General information

  • SDK/Library version: 2.8.0
  • Environment: Both
  • Language, language version, and OS: Node 8.9

Issue description

When calling methods that return a promise, they currently return successfully even if the call failed. I would think results should never contain an error.

This is the code I would like to write:

try {
  const results = await braintree.clientToken.generate({ customerId: 'invalid' });
  doThingsWithValidResults(results);
} catch (err) {
  handleError(err);
}

The example provided in the README defeats the purpose of Promises, as we now have to handle failure in two places:

promise().then((results) => {
  // anti-pattern
  if (result.success) {
    console.log('Transaction ID: ' + result.transaction.id);
  } else {
    handleFailureHere(results.message);
  }
}).catch((err) => {
  handleFailureHereToo(err);
});

Maybe this is a consequence of converting this library to support promises after initially being written in the err, response callback fashion. Would just like to see what y’all think about this.

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Comments:5 (3 by maintainers)

github_iconTop GitHub Comments

2reactions
crookedneighborcommented, Mar 2, 2018

Braintree’s underlying API that the different server SDKs use follows a result object pattern.

So, if you have a transaction that succeeds, you get:

gateway.transaction.sale({
  amount: '5.00',
  paymentMethodNonce: 'fake-valid-nonce',
  options: {
    submitForSettlement: true
  }
}).then((result) => {
  // result.success === true
  // result.transaction is the transaction object
})

Now, lets say you have a transaction that is processor declined:

gateway.transaction.sale({
  // sandbox amount to trigger processor declined 
  // https://developers.braintreepayments.com/reference/general/testing/node#test-amounts
  amount: '2000.00', 
  paymentMethodNonce: 'fake-valid-nonce',
  options: {
    submitForSettlement: true
  }
}).then((result) => {
  // result.success === false
  // result.transaction is still the transaction object
})

In this case, even though the transaction failed, a transaction object is still created, so it makes sense that the promise resolves, but reports that the sale was not a success.

There are only a few cases where the promise will actually reject. One is if the Braintree Gateway cannot be reached (no internet, revoked/incorrect API keys, rate limiting):

braintree.transaction.sale(saleOptions).catch((err) => {
  // you'll get here because the Braintree gateway could not be reached
  // or the API keys are invalid
  // or there have been too many requests made, etc
});

Or if the resource could not be found:

gateway.transaction.find('bad-id').catch((err) => {
  // this will result in a not found error
});

In basically every other case, the promise will resolve with result.success being true or false.

This pattern works very well for transactions, where you need a record of the attempted transaction even if the transaction did not actually charge the customer. It works less well for the example you gave:

braintree.clientToken.generate({ customerId: 'invalid' }).then((res) => {
  // res.sucess === false
  // res.message === 'Customer specified by customer_id does not exist',
});

Where there’s no underlying object being created.

I think we used this pattern largely because the other Braintree server SDKs used this pattern. And the reason the other SDKs used this pattern was because the alternative was to have the SDK raise an exception in the case where a transaction failed (because those languages don’t have the same async patterns as node, so you literally have to throw an error if the request failed).

I agree that this pattern is not perfect, but right now we can’t change it without both a major version bump and having a big inconsistency with the other server sdks.

0reactions
crookedneighborcommented, Jul 13, 2020

We’re now working on the next major version of the Node SDK, so we’re revisiting issues like this one.

We’ve decided to not provide support for this pattern. This doesn’t throw because the transaction object is created, there was no error in the underlying API request, it’s just that the status of the transaction was not a success and should be handled appropriately.

Even if we moved the logic into the error handler, all it does is move the complexity to a different location. You’d still need to parse the error and determine if it failed because the underlying API request failed, or if the transaction object was created, but it was declined. Beyond that, we’d need to attach the transaction (and/or any other resource object) to the error, and we think it’s a cleaner implementation to retain the pattern we have.

On y’all’s end, you could push the logic around res.success === false into the catch block this way:

gateway.transaction.sale(saleOptions).then((res) => {
  if (!res.success) {
    return Promise.reject(SOME_TRANSACTION_FAILED_OBJECT);
  }
  // handle the success
}).catch((err) => {
  // parse the err and determine how to handle the state of the transaction
});
Read more comments on GitHub >

github_iconTop Results From Across the Web

Balenciaga's child ad scandal to Kanye West's antisemitic ...
Balenciaga's child ad scandal to Kanye West's antisemitic meltdown: The 7 biggest fashion industry fails in 2022. BYSophie Mellor.
Read more >
State of Fashion | McKinsey
After experiencing 18 months of robust growth (early 2021 through mid-2022), the fashion industry is again facing a challenging climate.
Read more >
What Is A Brand Promise? 12 Powerful Examples - Gary Fox
A brand promise is a short simple statement that tells a customer what they can expect from your brand.
Read more >
The Myth of Sustainable Fashion - Harvard Business Review
Statements from fast fashion brands such as Primark (a retailer of $3.50 T shirts) that promise to “make more sustainable fashion affordable for ......
Read more >
JavaScript Promises - reject vs. throw - Stack Overflow
There is no advantage of using one vs the other, but, there is a specific case where throw won't work. However, those cases...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found