Skip to main content

Error Handling

It’s important to anticipate errors your users may encounter as you develop on Stellar. In many tutorials throughout our developer documentation, we leave out error handling code to focus on the example. In this section, we will do the opposite and talk specifically about the errors.

By the end of this section, you should be able to categorize errors and understand the best way to handle them in your application.

There are two main parts to this section:

  1. Resolution strategies — recommended resolution strategies that apply to most error scenarios you may encounter
  2. Managing specific errors — a deeper dive into the errors themselves. Refer to this section if you have encountered a specific error and want a better understanding of its cause.

For a fuller list of errors, see the Error section in our API documentation.

Part 1: resolution strategies

Many actions interact with the Stellar network through the Horizon API, and these possible actions fall into two main categories: 1. Queries (any GET request, like to /accounts) and 2. Transaction submissions (a POST /transactions).

There are many possible error codes when executing these actions, and you can typically handle these error codes using the following strategies:

  • Request adjustments: adjusting the request to resolve structural errors with queries or transaction submissions. Suppose you’ve included a bad parameter, malformed your XDR, or otherwise didn’t follow the endpoint’s specification. In these cases, resolve the error by referencing the details or result codes of the error response.
  • Retrying until success: this is the recommended way to work around latency or congestion issues encountered along the pipeline between your computer and the Stellar network, which can sometimes happen due to the nature of the distributed system.
  • Adjusting the transaction: this can also resolve issues but must be done with extreme care. If one of the above scenarios is in effect, it can trigger destructive duplicate actions (like sending a payment twice).

Let’s get into these strategies in more detail. We will mainly focus on the transaction submission category for each strategy since queries only return a read-only request.

Request adjustments strategy

Queries

Many GET requests have specific parameter requirements, and while the SDKs can help enforce them, you can still pass invalid arguments (for example, an asset string that isn’t SEP-11 compatible) that error out every time. In this scenario, there’s nothing you can do aside from following the API specification. The extras field of the error response will often clue you in on where to look and what to look for.

curl -s https://horizon-testnet.stellar.org/claimable_balances/0000 | jq '.extras'
{
"invalid_field": "id",
"reason": "Invalid claimable balance ID"
}

Note that the SDKs make it a point to distinguish an invalid request (as above) versus a missing resource (a 404 Not Found) (for example, the generic NetworkError versus a NotFoundError in the JavaScript SDK), where the latter might not be considered an error depending on your situation.

Transaction submissions

Certain transaction submission failures also need adjustments to succeed. If the XDR is malformed, or the transaction is otherwise invalid, you’ll encounter a 400 Bad Request (for example, see excluding a source account). Both transactions and their operations can be easily malformed: look at the extras.result_codes field for details and cross-reference them with the appropriate result codes documentation to determine specifics.

Transaction fees are also a safe adjustment. If you get a tx_insufficient_fee error, refer to the Insufficient Fees and Surge Pricing section later in this document.

Retrying until success strategy

Transaction submissions

There are many possible scenarios (504 Timeouts, transient outages, congestion on the network) in which retrying your transaction submission is the only reasonable solution. However, only use this method after trying to make safe modifications to the transaction.

There is no way to cancel a transaction after submitting it. So, successfully resubmitting your transaction has two considerations. 1. Time bounds. Time bounds are optional but recommended, as they put a time limit on the transaction- so either the transaction makes it onto the ledger or it times out depending on your time parameters. 2. If the transaction has already successfully made it to the ledger, Horizon will return any attempted resubmission. Only in cases where a transaction’s status is unknown (and thus will have a chance to make it on the ledger) will a resubmission to the network occur.

If a transaction successfully makes it into the ledger, any attempted resubmitted transactions will be returned to you.

Example scenario:

You submit a transaction, and it enters the queue of the Stellar network, but Horizon crashes while giving you a response. Uncertain about the transaction status, you resubmit the transaction (with no changes!) until either a. Horizon comes back up to give you a reply or b. your time bounds are exceeded.

There are only two possible results to this scenario: either the transaction makes it into the ledger (exactly once) and Horizon gives you the response, or the transaction never makes it out of the queue, and you receive the corresponding tx_too_late response.

Example implementation:

let server = sdk.Server("horizon.stellar.org");

function submitTransaction(tx, timeout) {
if (!tx.timeBounds || tx.timeBounds.maxTime === 0) {
throw new Error("Always set a reasonable timebound!");
}
const expiration = parseInt(tx.timeBounds.maxTime);

return server.submitTransaction(tx).catch(function (error) {
if (isNonRetryErrorCase(error)) {
// ...do other error handling...
return;
}

// the tx no longer has a chance of making it into a ledger
if (Date.now() >= expiration) {
return new Error("The transaction timed out.");
}

timeout = timeout || 1; // start the (linear) back-off process
return sleep(timeout).then(function () {
return submitTransaction(tx, timeout + 5);
});
});
}

We assume the existence of a sleep implementation similar to the one here Be sure to integrate backoff into your retry mechanism. In our example error-handling code above, we implement a simple linear backoff, but there are plenty of recommendations for various other strategies. Backoff is important both for maintaining performance and avoiding rate-limiting issues.

Adjusting the transaction- unsafe transaction adjustments strategy

Transaction submissions

Resubmitting an unchanged transaction (same operations, signatures, sequence number, etc.) is always safe to do. However, be careful when working around an error that does require changes to the transaction. It can cause duplicate transactions, which can cause problems (double payments, incorrect trustlines, and more).

Example scenario: invalid sequence numbers

These errors typically occur when you have an outdated view of an account. This could be because multiple devices are using this account, you have concurrent submissions happening, or other reasons. The solution is relatively simple: retrieve the account details and try again with an updated sequence number.

// suppose `account` is an outdated `AccountResponse` object
let tx = sdk.TransactionBuilder(account, ...)/* etc */.build()
server.submitTransaction(tx).catch(function (error)) {
if (error.response && error.status == 400 && error.extras &&
error.extras.result_codes.transaction == sdk.TX_BAD_SEQ) {
return server.accounts()
.accountId(account.accountId())
.then(function (response) {
let tx = sdk.TransactionBuilder(response, ...)/* etc */.build()
return server.submitTransaction(tx);
});
}
// ...other error conditions...
}

Despite the solution’s simplicity, things can go wrong fast if you don’t understand why the error occurred.

Suppose you submit transactions from multiple places in your application simultaneously, and your user spammed the Send Payment button a few times in their impatience. If you send the exact same payment transaction for each tap, naturally, only one will succeed. The others will fail with an invalid sequence number (tx_bad_seq), and if you resubmit blindly with an updated sequence number (as we do above), these payments will also succeed, resulting in more than one payment being made when only one was intended. So, be very careful when resubmitting transactions that have been modified to work around an error.

Part 2: managing specific errors

Here, we will cover specific errors commonly encountered during transaction submission and direct you to the appropriate resolution. We’ll start with a table of common errors and their codes and descriptions, then dive deeper into some specific ones.

ResultCodeDescription
FAILED-1One of the operations failed (see List of Operations for errors)
TOO_EARLY-2Ledger closeTime before minTime value in the transaction
TOO_LATE-3Ledger closeTime after maxTime value in the transaction
MISSING_OPERATION-4No operation was specified
BAD_SEQ-5Sequence number does not match source account
BAD_AUTH-6Too few valid signatures / wrong network
INSUFFICIENT_BALANCE-7Fee would bring account below minimum balance; see our section on Lumens for more info
NO_ACCOUNT-8Source account not found
INSUFFICIENT_FEE-9Fee is too small; see our section on Fees for more info
BAD_AUTH_EXTRA-10Unused signatures attached to transaction
INTERNAL_ERROR-11An unknown error occurred
NOT_SUPPORTED-12The transaction type is not supported
FEE_BUMP_INNER_FAILED-13The fee bump inner transaction failed
BAD_SPONSORSHIP-14The sponsorship is not confirmed

We’ll do a deeper dive into the following errors:

  • Timeouts: 504 Timeout
  • Insufficient fees and surge pricing: INSUFFICIENT_FEE
  • Rate limiting: 429 Too Many Requests
  • Insufficient XLM balance: INSUFFICIENT_BALANCE

Timeouts

Horizon may send a 504 Timeout after transaction submission. Timeouts are not errors but warnings that your request hasn’t been fulfilled yet. This can happen because of the relationship between Horizon and Stellar Core- the network may take some time (5-10 mins during congestion) to accept the transaction. At the same time, Horizon needs to provide developers with a response within 30 seconds.

Receiving a 504 for your transaction submission does not mean the transaction didn’t make it to the network. Continue with retries until you get a definitive response. If you continue to face timeouts on retries, consider using a fee-bump transaction to get into the ledger (after the time bounds expire) or increasing the maximum fee you’re willing to pay. Read up on Surge Pricing and Fee Strategies for more details.

Insufficient fees and surge pricing

See the Surge Pricing and Fee Strategies Encyclopedia Entry

Rate limiting

If you’re using SDF’s public Horizon instance, you may get a 429 Too Many Requests error when exceeding the rate limits. If you’re encountering this frequently, it may be time to deploy your own Horizon instance!

Insufficient XLM balance

Any transaction that would reduce an account’s balance to less than the minimum will be rejected with an INSUFFICIENT_BALANCE error. Likewise, lumen selling liabilities that would reduce an account’s balance to less than the minimum plus lumen selling liabilities will be rejected with an INSUFFICIENT_BALANCE error.

For more on minimum balances, see our Lumens section.