Search code examples
node.jsbraintree

Node.js: Why Braintree methods resolve promise with error response?


In Braintree Node.js SDK, the methods will return a promise if no callback is given. For example:

1st time

gateway.customer.create({
        firstName: "First",
        lastName: "Last",
        email: "test@abc.com",
        id: "123123"
    })
    .then((response) => {
        console.log(response); // correct response from this line
    })
    .catch((err) => {
        console.error(err);
    });

It will log the response correctly.

After that, I execute the same code again. I expect a fail because of duplicate custom ID.

2nd time

gateway.customer.create({
        firstName: "First",
        lastName: "Last",
        email: "test@abc.com",
        id: "123123"
    })
    .then((response) => {
        console.log(response); // error response from this line
    })
    .catch((err) => {
        console.error(err);
    });

It will log the error response in the then block.

3rd time

gateway.customer.find("1231234") // not existing
    .then((response) => {
        console.log(response);
    })
    .catch((err) => {
        console.error(err); // error from this line
    });

It will log the Not Found from catch block.

Is there any reason the 2nd give a resolve with an error while the 3rd give a reject? Is it safe if I write a promisifier and treat all response with success: false as reject?


Solution

  • This answer comes from github, which explain the issue perfectly. https://github.com/braintree/braintree_node/issues/130#issuecomment-370079910

    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.