|
| 1 | +# Error Handling |
| 2 | + |
| 3 | +The Hiero Python SDK raises structured exceptions at three distinct stages of the transaction lifecycle. Understanding when each exception is thrown and what information it carries helps you write resilient applications. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +Every transaction you submit passes through three stages, each of which can raise a distinct exception: |
| 8 | + |
| 9 | +1. **Pre-consensus validation (precheck)** — the receiving node validates the transaction before forwarding it to the network. Failures here raise `PrecheckError`. |
| 10 | +2. **Network retry** — if the node is busy or unreachable, the SDK retries automatically. When retries are exhausted, `MaxAttemptsError` is raised. |
| 11 | +3. **Receipt validation** — after consensus, the SDK fetches a receipt and checks its status. A non-`SUCCESS` status raises `ReceiptStatusError`. |
| 12 | + |
| 13 | +## PrecheckError |
| 14 | + |
| 15 | +**When raised:** A node rejected the transaction before it reached consensus. Common causes are an insufficient fee, an expired transaction, or an invalid signature. |
| 16 | + |
| 17 | +**Attributes:** |
| 18 | + |
| 19 | +| Attribute | Type | Description | |
| 20 | +|---|---|---| |
| 21 | +| `status` | `ResponseCode` | The precheck status code returned by the node | |
| 22 | +| `transaction_id` | `TransactionId or None` | The ID of the rejected transaction | |
| 23 | +| `message` | `str` | Human-readable error string | |
| 24 | + |
| 25 | +**Common status codes:** |
| 26 | + |
| 27 | +- `INSUFFICIENT_TX_FEE` — the attached fee is too low |
| 28 | +- `INVALID_SIGNATURE` — one or more signatures are missing or incorrect |
| 29 | +- `TRANSACTION_EXPIRED` — the transaction valid window has passed |
| 30 | +- `PAYER_ACCOUNT_NOT_FOUND` — the payer account does not exist |
| 31 | + |
| 32 | +**Example:** |
| 33 | + |
| 34 | +```python |
| 35 | +from hiero_sdk_python.exceptions import PrecheckError |
| 36 | + |
| 37 | +try: |
| 38 | + transaction.execute(client) |
| 39 | +except PrecheckError as e: |
| 40 | + print(f"Precheck failed: {e.status.name} ({int(e.status)})") |
| 41 | + print(f"Transaction ID: {e.transaction_id}") |
| 42 | +``` |
| 43 | + |
| 44 | +## MaxAttemptsError |
| 45 | + |
| 46 | +**When raised:** The SDK exhausted all retry attempts without a successful response. This typically occurs when a node is consistently busy (`BUSY` status) or when a network timeout is hit on every attempt. |
| 47 | + |
| 48 | +**Attributes:** |
| 49 | + |
| 50 | +| Attribute | Type | Description | |
| 51 | +|---|---|---| |
| 52 | +| `node_id` | `str` | The node that was being contacted on the final attempt | |
| 53 | +| `last_error` | `BaseException or None` | The underlying error from the last attempt (may be a gRPC error or another exception) | |
| 54 | +| `message` | `str` | Human-readable error string | |
| 55 | + |
| 56 | +**Note:** `last_error` wraps the root cause — it may be a `PrecheckError` (from a node returning `BUSY`) or a low-level gRPC transport error. |
| 57 | + |
| 58 | +**Example:** |
| 59 | + |
| 60 | +```python |
| 61 | +from hiero_sdk_python.exceptions import MaxAttemptsError |
| 62 | + |
| 63 | +try: |
| 64 | + transaction.execute(client) |
| 65 | +except MaxAttemptsError as e: |
| 66 | + print(f"All attempts exhausted. Last node tried: {e.node_id}") |
| 67 | + if e.last_error: |
| 68 | + print(f"Root cause: {e.last_error}") |
| 69 | +``` |
| 70 | + |
| 71 | +## ReceiptStatusError |
| 72 | + |
| 73 | +**When raised:** The transaction reached consensus but the network returned a non-`SUCCESS` status in the receipt. This is the most common error for logic failures (e.g. insufficient balance, token not associated). |
| 74 | + |
| 75 | +**Attributes:** |
| 76 | + |
| 77 | +| Attribute | Type | Description | |
| 78 | +|---|---|---| |
| 79 | +| `status` | `ResponseCode` | The error status from the receipt | |
| 80 | +| `transaction_id` | `TransactionId or None` | The ID of the failed transaction | |
| 81 | +| `transaction_receipt` | `TransactionReceipt` | The full receipt object | |
| 82 | +| `message` | `str` | Human-readable error string | |
| 83 | + |
| 84 | +**Note:** Receipt validation is controlled by the `validate_status` parameter on `get_receipt()`. When set to `False`, the method returns the receipt without raising even for non-`SUCCESS` statuses. |
| 85 | + |
| 86 | +**Example:** |
| 87 | + |
| 88 | +```python |
| 89 | +from hiero_sdk_python.exceptions import ReceiptStatusError |
| 90 | + |
| 91 | +try: |
| 92 | + receipt = transaction.execute(client).get_receipt(client) |
| 93 | +except ReceiptStatusError as e: |
| 94 | + print(f"Transaction failed at consensus: {e.status.name} ({int(e.status)})") |
| 95 | + print(f"Transaction ID: {e.transaction_id}") |
| 96 | + print(f"Full receipt: {e.transaction_receipt}") |
| 97 | +``` |
| 98 | + |
| 99 | +## Understanding ResponseCode |
| 100 | + |
| 101 | +`ResponseCode` is an `IntEnum` with approximately 394 named values representing every status the Hedera network can return. |
| 102 | + |
| 103 | +**Key values:** |
| 104 | + |
| 105 | +| Name | Integer | Meaning | |
| 106 | +|---|---|---| |
| 107 | +| `OK` | 0 | Request accepted (not yet processed) | |
| 108 | +| `BUSY` | 12 | Node is too busy; SDK will retry | |
| 109 | +| `UNKNOWN` | 21 | Status could not be determined | |
| 110 | +| `SUCCESS` | 22 | Transaction succeeded | |
| 111 | + |
| 112 | +**Accessing values:** |
| 113 | + |
| 114 | +```python |
| 115 | +from hiero_sdk_python.response_code import ResponseCode |
| 116 | + |
| 117 | +code = ResponseCode.INSUFFICIENT_TX_FEE |
| 118 | +print(code.name) # "INSUFFICIENT_TX_FEE" |
| 119 | +print(int(code)) # integer value |
| 120 | + |
| 121 | +# Synthetic codes (returned when the status is unrecognised) expose is_unknown |
| 122 | +if code.is_unknown: |
| 123 | + print("Unrecognised status code") |
| 124 | +``` |
| 125 | + |
| 126 | +**Note:** `ResponseCode` does not include built-in human-readable descriptions beyond the enum name. Refer to the [Hedera status code reference](https://docs.hedera.com/hedera/sdks-and-apis/hedera-api/miscellaneous/responsecode) for full descriptions. |
| 127 | + |
| 128 | +## Practical Example |
| 129 | + |
| 130 | +```python |
| 131 | +from hiero_sdk_python.exceptions import PrecheckError, MaxAttemptsError, ReceiptStatusError |
| 132 | +from hiero_sdk_python.response_code import ResponseCode |
| 133 | + |
| 134 | +try: |
| 135 | + receipt = transaction.execute(client).get_receipt(client) |
| 136 | + print("Transaction succeeded!") |
| 137 | +except PrecheckError as e: |
| 138 | + # Rejected before consensus — fix the transaction and resubmit |
| 139 | + print(f"Precheck failed: {e.status.name}") |
| 140 | + print(f"Transaction ID: {e.transaction_id}") |
| 141 | +except ReceiptStatusError as e: |
| 142 | + # Reached consensus but failed — inspect the receipt for details |
| 143 | + print(f"Consensus failure: {e.status.name}") |
| 144 | + print(f"Transaction ID: {e.transaction_id}") |
| 145 | +except MaxAttemptsError as e: |
| 146 | + # Network issue — safe to retry after a backoff |
| 147 | + print(f"Network error on node {e.node_id}: {e.last_error}") |
| 148 | +``` |
| 149 | + |
| 150 | +## Retry and Backoff Guidance |
| 151 | + |
| 152 | +**Automatically retried by the SDK:** |
| 153 | + |
| 154 | +The SDK retries requests internally when a node returns transient statuses such as `BUSY` or `PLATFORM_NOT_ACTIVE`. You do not need to handle these yourself. |
| 155 | + |
| 156 | +**Do not retry without fixing the root cause:** |
| 157 | + |
| 158 | +- `INSUFFICIENT_TX_FEE` — increase the transaction fee |
| 159 | +- `INVALID_SIGNATURE` — verify that all required keys have signed |
| 160 | +- `TRANSACTION_EXPIRED` — create and sign a new transaction with a fresh valid window |
| 161 | +- `ACCOUNT_DELETED` / `TOKEN_NOT_ASSOCIATED_TO_ACCOUNT` — resolve the account or token state first |
| 162 | + |
| 163 | +**Manual retry is appropriate for `MaxAttemptsError`:** |
| 164 | + |
| 165 | +If `MaxAttemptsError` is raised due to a temporary network disruption, a manual retry with exponential backoff is safe: |
| 166 | + |
| 167 | +```python |
| 168 | +import time |
| 169 | +from hiero_sdk_python.exceptions import MaxAttemptsError |
| 170 | + |
| 171 | +for attempt in range(3): |
| 172 | + try: |
| 173 | + receipt = transaction.execute(client).get_receipt(client) |
| 174 | + break |
| 175 | + except MaxAttemptsError: |
| 176 | + if attempt == 2: |
| 177 | + raise |
| 178 | + time.sleep(2 ** attempt) # 1s, 2s, 4s backoff |
| 179 | +``` |
| 180 | + |
| 181 | +## See Also |
| 182 | + |
| 183 | +- [examples/errors/precheck_error.py](../../examples/errors/precheck_error.py) — runnable `PrecheckError` example |
| 184 | +- [examples/errors/receipt_status_error.py](../../examples/errors/receipt_status_error.py) — runnable `ReceiptStatusError` example |
| 185 | +- [examples/errors/max_attempts_error.py](../../examples/errors/max_attempts_error.py) — runnable `MaxAttemptsError` example |
0 commit comments