Skip to content

Commit 08784be

Browse files
docs: add user-facing error handling guide to docs/sdk_users/ (hiero-ledger#2249)
Signed-off-by: bhuvan-somisetty <somisettybhuvan5@gmail.com>
1 parent bd9a02e commit 08784be

3 files changed

Lines changed: 190 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ print(f"Balance: {balance.hbars} HBAR")
6060
### For SDK Users
6161

6262
- **[Running Examples](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_users/running_examples.md)** - Complete guide to all SDK operations with code examples
63+
- **[Error Handling](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_users/error_handling.md)** - Exception types, response codes, and retry guidance
6364
- **[Examples Directory](https://github.com/hiero-ledger/hiero-sdk-python/tree/main/examples)** - Ready-to-run example scripts
6465

6566
### For SDK Developers

docs/sdk_users/error_handling.md

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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

docs/sdk_users/running_examples.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2214,3 +2214,7 @@ record = (
22142214
print(f"Generated PRNG bytes length: {len(record.prng_bytes)} bytes")
22152215
print(f"PRNG bytes in hex: {record.prng_bytes.hex()}")
22162216
```
2217+
2218+
## See Also
2219+
2220+
- [Error Handling](error_handling.md) — how to catch and handle `PrecheckError`, `ReceiptStatusError`, and `MaxAttemptsError`

0 commit comments

Comments
 (0)