VM exception handling
How the TVM handles errors, the four exception categories, the call stack depth limit, and Solidity error-handling tools (assert, require, revert, custom errors).
Prerequisites
This page covers two related topics: how the TVM handles errors during contract execution, and how to write Solidity that handles errors cleanly. Sections 1–3 are reference material for diagnosing failed transactions; sections 4–6 are guidance for writing better contracts.
TVM exception types
There are four categories of exceptions that may occur during contract execution. Each has different consequences for Energy consumption and on-chain visibility.
Runtime exceptions that exhaust the Energy allowance
When a contract terminates because of OUT_OF_ENERGY, OUT_OF_TIME, invalid opcode, a VM stack overflow, an out-of-memory condition, or certain other TVM runtime exceptions, the TVM calls spendAllEnergy() and accounts for Energy up to the limit calculated before execution. Both the Energy used before the failure and the unused allowance are recorded as consumed.
Common conditions include:
- Energy exhaustion — the next operation requires more Energy than remains available.
- Execution timeout — execution exceeds the per-transaction CPU time limit, controlled by
getMaxCpuTimeOfOneTx(currently 80 ms on Mainnet). - Invalid opcode — execution reaches an unsupported or explicitly triggered
invalid opcode. - VM stack overflow — the TVM raises
JVMStackOverFlowException. - Memory limit exceeded — a single call frame allocates more than 3 MB; nested call frames within the same transaction are each subject to this limit.
Whether Solidity language-level checks enter this path depends on the compiler version. Before Solidity 0.8.0, failed assert calls and some arithmetic or bounds checks could compile to invalid opcode, exhausting the execution's Energy allowance. Solidity 0.8+ normally returns Panic(uint256) and terminates through REVERT, using the accounting described in the next section.
REVERT
REVERTREVERT can be triggered by require, revert, Solidity 0.8+ Panic(uint256), and other conditions. It only charges the Energy used up to the point of failure; the unused Energy allowance is not billed. The typical error message is REVERT opcode executed.
The following conditions trigger REVERT:
- Calling
require(expression)whereexpressionevaluates tofalse. - A function called via a message call that does not end correctly (for example, runs out of Energy or throws). If Energy is not specified at the call site, all available Energy is forwarded; from the outer result alone, the caller may be unable to tell whether the callee exhausted its Energy allowance or terminated through
REVERT. This rule does not apply to low-level calls such ascall,send, anddelegatecall, which returnfalseon failure rather than reverting automatically. - Creating a contract with
newwhere the constructor does not finish (you cannot specify Energy fornew, so all available Energy appears consumed). - Receiving TRX through a public function (constructor, fallback, or any public function) that lacks the
payablemodifier. - A failed
transfer()call. - A
revert()call. - A Solidity 0.8+
Panic(uint256)caused by anassertfailure, arithmetic overflow or underflow, division by zero, out-of-bounds array access, or another language-level check.
State rollback and Energy accountingAfter a contract enters TVM execution, state changes are rolled back whether execution terminates because of a runtime exception such as
OUT_OF_TIMEorinvalid opcode, or throughREVERT. This preserves transaction atomicity and data consistency.Energy accounting depends on how execution terminates:
- Runtime exceptions that call
spendAllEnergy(): Energy is accounted for up to the limit calculated before execution, including the unused remainder.REVERT: Only the Energy used before the error is charged; the unused allowance is not billed.Energy already consumed is not refunded under either accounting path.
Validation-style exception
Validation-style exceptions are caught by the FullNode before the transaction reaches the TVM. The transaction is not recorded on-chain and no Energy or Bandwidth is consumed.
These exceptions occur when:
- The current TVM version does not support a feature the contract requires.
- A contract being created has a name longer than 32 bytes.
- A contract being created has a
consume_user_resource_percentoutside the valid range[0, 100]. - A contract being created hits a hash collision in the generated contract address.
- The
call_valueis non-zero and exceeds the caller's balance. - The
fee_limitis outside the valid range. - A constant request is sent to a node that does not support constant calls.
- The triggered contract does not exist in the database.
Illegal VM operation exception
These exceptions are also not recorded on-chain, but the FullNode that broadcast the transaction is penalized at the network layer for a period of time (to prevent spam):
OwnerAddressandOriginAddressmismatch during contract creation.- Broadcasting a constant request (constant calls should not be broadcast).
Stack depth and recursive call limits
The TVM limits contract-call depth to 64 layers, covering cross-contract calls, nested calls, and recursive self-calls. At the limit, the TVM does not enter another call frame: a low-level CALL returns false, and CREATE returns the zero address, without directly forcing the outer execution to REVERT. Solidity high-level contract calls normally check the failure result and revert, while callers using low-level call can inspect the returned false and decide how to handle it.
A separate but related limit is the Solidity compiler's Stack too deep error, which is a compile-time limitation triggered by too many local or temporary variables. This is not directly related to recursion at runtime — but it serves as a reminder to decompose complex functions, reduce local variables, or manually manage stack frames.
When designing recursive logic:
- Cap the recursion depth explicitly to prevent runaway recursion.
- Estimate per-call Energy so the total stays within
fee_limit. - Prefer iteration (
for/whileloops) or transaction splitting where possible — they almost always cost less Energy than equivalent recursion.
Solidity error handling tools
Solidity provides four mechanisms for error handling: assert, require, revert, and (since 0.8.4) Custom Errors.
assert
assertassert is for internal invariants that should never be violated. Use it for internal bugs and invariant violations, not as a substitute for require when validating input or permissions. The error format and Energy accounting after an assert failure depend on the Solidity compiler version: older versions commonly emit invalid opcode, while Solidity 0.8+ returns Panic(uint256) and terminates through REVERT.
assert(x == y);Since Solidity 0.8.0, the compiler inserts a Panic(uint256) error for assert failures.
require
requirerequire is for validating external inputs, access control, and other preconditions. A require failure reverts state changes, consumes only Energy used so far, and returns the remainder.
require(msg.sender == owner, "Not the owner");The error message string is shown in transaction logs and helps users diagnose the failure.
revert
revertrevert aborts execution at any point, with an optional reason string or custom error. It is the most flexible of the four — useful when a require would not naturally fit the control flow.
if (someCondition) {
revert("Condition not met");
}Custom errors (Solidity 0.8.4+)
Custom errors give you two advantages over string-based revert reasons:
- Lower Energy cost — error selectors are 4 bytes, much cheaper to encode than a long string.
- Structured parameters — callers receive typed data (an address, a uint256, etc.) they can use to build a specific error UI.
// Declare the error
error Unauthorized(address caller);
contract MyContract {
address public owner;
constructor() {
owner = msg.sender;
}
function doSomething() external {
if (msg.sender != owner) {
revert Unauthorized(msg.sender);
}
// Other logic
}
}When Unauthorized(msg.sender) reverts, the TVM rolls back the transaction and returns the unused Energy. Off-chain callers (TronWeb, Trident) decode the selector and parameters automatically when an ABI is available.
Best practices
Energy cost matters in error handling
The termination path directly affects Energy accounting:
- In Solidity 0.8+, an
assertfailure returnsPanic(uint256)and terminates throughREVERT, so only Energy used before the failure is charged. Compilers before 0.8.0 commonly emittedinvalid opcodefor failed assertions, which can exhaust the execution's Energy allowance. requireand explicitrevertcharge only the Energy used before failure and are appropriate for normal business validation.- Custom errors save Energy at the encoding/decoding level versus string-based reverts. Prefer them for any contract that reverts often.
In hot paths, validate cheap-to-check inputs first to avoid wasting Energy on later computation that would be reverted anyway.
Use assert only for invariants
assert only for invariantsIf you find yourself using assert for input validation, switch to require instead. assert failures should indicate a bug in the contract — never a normal user-facing error.
Use require and custom errors for validation
require and custom errors for validationrequire (and the equivalent revert with a reason or custom error) is the right tool for:
- Access control (
require(msg.sender == owner, ...)) - Input bounds (
require(amount > 0, ...)) - Contract state preconditions (
require(state == State.Open, ...))
Reduce recursion when you can
The TVM's 64-layer call stack and Energy cost both penalize deep recursion. Many recursive algorithms have iterative equivalents that cost less Energy and are easier to reason about. Reach for recursion only when the iterative form is significantly more complex.
Reentrancy considerations
External calls — including recursive self-calls and contract-to-contract calls — surrender execution control to the callee. The callee can re-enter your contract before your function finishes, leading to state-race vulnerabilities (the most famous being the DAO hack on Ethereum).
Use the checks-effects-interactions pattern: validate preconditions first, update state next, then make external calls last. For sensitive paths, consider OpenZeppelin's ReentrancyGuard or equivalent. See Best practices for more.
Related resources
- Contract-to-contract calls — call patterns and reentrancy
- Parameter encoding and decoding — function selectors and ABI encoding (custom errors use 4-byte selectors)
- Opcodes —
REVERT,INVALID, and stack-related opcodes - FeeLimit & Energy cost — set
fee_limitto bound the cost when a runtime exception exhausts the Energy allowance - Best practices — broader security checklist
Updated 4 days ago