Error Handling
Hiero throws typed exceptions that tell you exactly what went wrong, whether to retry, and what to fix.
Exception hierarchy
| Exception | When thrown | Transient? |
|---|---|---|
PrecheckException |
Gateway node rejects the transaction before consensus | Some codes (see below) |
TransactionException |
Transaction reached consensus but the result is non-success | No — fees already charged |
ConsensusException |
Transaction didn't reach consensus within TransactionDuration |
Always — safe to retry |
ContractException |
Smart contract call/query failed (EVM revert, out-of-gas) | Rarely — usually permanent |
MirrorGrpcException |
Mirror gRPC stream terminated unexpectedly | Unavailable / CommunicationError yes; TopicNotFound no |
MirrorException |
Mirror REST API returned an HTTP error | 429/503/504 yes; 400/404 no |
Basic error handling
try
{
var receipt = await client.TransferAsync(from, to, amount);
}
catch (PrecheckException ex) when (ex.Status == ResponseCode.InsufficientTxFee)
{
// The fee was too low — increase FeeLimit and retry
Console.WriteLine($"Need at least {ex.RequiredFee} tinybars");
}
catch (PrecheckException ex) when (IsTransient(ex.Status))
{
// Gateway is busy — back off and retry
await Task.Delay(TimeSpan.FromSeconds(2));
}
catch (PrecheckException ex)
{
// Permanent precheck failure — fix the request
Console.WriteLine($"Precheck failed: {ex.Status}");
}
catch (TransactionException ex)
{
// Reached consensus but failed — do NOT retry
Console.WriteLine($"Transaction failed: {ex.Status}");
Console.WriteLine($"Receipt: {ex.Receipt}");
}
catch (ConsensusException)
{
// Timed out — always safe to retry
Console.WriteLine("Consensus timeout, retrying...");
}
static bool IsTransient(ResponseCode code) =>
code is ResponseCode.Busy
or ResponseCode.PlatformTransactionNotCreated;
Suppress TransactionException
For workflows where you want to inspect the receipt yourself rather than catching exceptions:
var receipt = await client.TransferAsync(from, to, amount, ctx =>
{
ctx.ThrowIfNotSuccess = false;
});
if (receipt.Status != ResponseCode.Success)
{
Console.WriteLine($"Non-success: {receipt.Status}");
// handle without an exception
}
Retry with resilience pipelines
builder.Services.AddResiliencePipeline("hedera", pipeline =>
{
pipeline.AddRetry(new RetryStrategyOptions
{
MaxRetryAttempts = 3,
Delay = TimeSpan.FromSeconds(1),
BackoffType = DelayBackoffType.Exponential,
ShouldHandle = new PredicateBuilder()
.Handle<PrecheckException>(ex =>
ex.Status is ResponseCode.Busy
or ResponseCode.PlatformTransactionNotCreated)
.Handle<ConsensusException>()
});
});
Timeouts with CancellationToken
Most SDK methods accept a CancellationToken (either directly or on the *Params object):
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var receipt = await client.TransferAsync(from, to, amount, ctx =>
{
// The per-call configure callback can also set transaction-level timeouts
ctx.TransactionDuration = TimeSpan.FromSeconds(60);
});
Mirror node error handling
try
{
var data = await mirror.GetAccountAsync(account);
}
catch (MirrorException ex) when (ex.StatusCode == HttpStatusCode.TooManyRequests)
{
// Rate limited — back off and retry
await Task.Delay(TimeSpan.FromSeconds(5));
}
catch (MirrorException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
// Entity doesn't exist (or hasn't propagated to the mirror yet)
}
See also
PrecheckExceptionAPI reference — full<remarks>with code-by-code guidanceTransactionExceptionAPI referenceResponseCodeenum — every Hedera status code