Error Handling
Recoverable Errors
Recoverable errors represent expected faults. Similar to Rust, Sway expresses recoverable errors by using the std::result::Result enum, letting you propagate or transform the error without immediately reverting the transaction.
To learn more about expressing and handling recoverable errors, see the chapter on Result<T, E> enum.
Irrecoverable Errors
Irrecoverable errors indicate bugs or violated invariants. They trigger a VM-wide revert that atomically rolls back every state change in the transaction, and it cannot be caught or handled in Sway code, signaling that the program cannot sensibly continue.
panic Expression
The recommended way of expressing an irrecoverable errors is to use the panic expression:
if some_error_occurred {
panic "Some error has occurred.";
}
At runtime, the panic expression aborts and reverts the execution of the entire program. At compile time, for each panic encountered in code, Sway compiler will generate a unique revert code and create an entry in the ABI JSON errorCodes section. The generated errorCodes entry will contain the information about the source location at which the panic occurs, as well as the error message.
In addition, for each function call that might panic, compiler will generate an entry in the ABI JSON panickingCalls section. Those entries will contain source locations of functions whose calls might eventually end up in calling a panic somewhere in the call chain.
Combined together, these two ABI JSON entries, errorCodes and panickingCalls, allow for getting a rich troubleshooting information, that contains the error location and a partial backtrace, without any or negligible additional on-chain cost.
The generated bytecode will contain only the revert instructions, while error messages and error locations will be stored off-chain, in the ABI JSON file, making the panicking a zero on-chain cost operation. Panic backtrace comes with only a negligible on-chain cost, that can additionally be opted-in or out, as explained in detail in the chapter Configuring the Backtrace Content.
Partial backtrace means that the backtrace is limited to up to five function calls. In practice, deeper call chains are rare. Also, it is possible to choose which functions should be a part of the reported backtrace. This is done by using the backtrace build option and the #[trace] attribute, as also explained in detail in the chapter Configuring the Backtrace Content.
Tools like Rust and TypeScript SDKs and forc test recognize revert codes generated by panic expressions. E.g., if a Sway unit test fails because of a revert caused by the above panic line, forc test might display an output similar to the following:
test some_test, "/tests.sw":42
revert code: 8100000000000000
├─ panic message: Some error has occurred.
├─ panicked: in some_package::some_error_occurred
│ └─ at some_package@1.2.3, src/some_module.sw:13:9
└─ backtrace: called in some_other_package::some_other_module::some_function
└─ at some_other_package@1.2.3, src/some_other_module.sw:106:11
called in my_project::some_test
└─ at my_project, src/tests.sw:48:8
What the above panic location and the backtrace are telling us, is that some_test has called some_function that has called some_error_occurred which has panicked with the message "Some error has occurred."
Error Types
Passing textual error messages directly as a panic argument is the most convenient way to provide a helpful error message. It is sufficient for many use-cases. However, sometimes we want:
- to provide an additional runtime information about the error,
- or to group a certain family of errors together.
For these use-cases, you can use error types. Error types are enums annotated with the #[error_type] attribute, whose all variants are attributed with the #[error(m = "<error message>")] attributes. Each variant represent a particular error, and the enum itself the family of errors. The convention is to postfix the names of error type enums with Error.
For example, let's assume we are checking if a provided Identity has certain access rights to our contract. The error type enum representing access rights violations could look like:
#[error_type]
pub enum AccessRightError {
#[error(m = "The provided identity is not an administrator.")]
NotAnAdmin: Identity,
#[error(m = "The provided identity is not an owner.")]
NotAnOwner: Identity,
#[error(m = "The provided identity does not have write access.")]
NoWriteAccess: Identity,
}
where each Identity represents the actual, provided identity.
In code, we can now check for access rights and panic if they are violated:
fn do_something_that_requires_admin_access(admin: Identity) {
if !is_admin(admin) {
panic AccessRightError::NotAnAdmin(admin);
}
// ...
}
Assuming we have a failing test for the above function, the test output will show the error message, but also the provided Identity as the panic value. E.g.:
test some_test_for_admin_access, "/test.sw":42
revert code: 8100000000000000
├─ panic message: The provided identity is not an administrator.
├─ panic value: NotAnAdmin(Address(Address(79fa8779bed2f36c3581d01c79df8da45eee09fac1fd76a5a656e16326317ef0)))
├─ panicked: in auth_package::only_admin
│ └─ at auth_package@0.1.0, src/admin_access.sw:11:9
└─ backtrace: ...
errorCodes and panickingCalls ABI JSON Entries
To explain how errorCodes and panickingCalls are used to provide error information and backtrace, let's consider the following example:
- a function
only_adminis defined in theauth_packageand it panics if the identity is not an admin. only_adminis called in various guard functions that check preconditions, defined in theguards_package. E.g.,check_access_rights.- those
guards_packagefunctions are used within thefunds_contract.
At compile time, an entry similar to these will be added to the ABI JSON errorCodes and panickingCalls sections:
"errorCodes": {
"0": { // Unique ID of a `panic` call.
"pos": { // Location in code, at which the `panic` call occurs.
"pkg": "auth_package@1.2.3",
"function": "auth_package::admin_access::only_admin",
"file": "src/admin_access.sw",
"line": 13,
"column": 9
},
"logId": "10098701174489624218", // Log ID representing the `AccessRightError` enum
// passed as an argument to `panic`.
"msg": null,
},
// Other error codes for other `panic` calls.
},
"panickingCalls": {
"1": { // Unique ID of a potentially panicking function call.
"pos": { // Location in code, at which the function call that might panic occurs.
"function": "guards_package::preconditions::check_admin", // The caller function, `check_admin`.
"pkg": "guards_package@0.1.0",
// Position within the `check_admin` where `only_admin` is called.
"file": "src/preconditions.sw",
"line": 4,
"column": 9
},
"function": "auth_package::admin_access::only_admin" // The called function, `only_admin`.
},
// Other panicking calls.
}
Those unique error and panicking call IDs are embedded by the compiler into the revert code generated for a particular panic call.
In case of a revert, tools like SDKs and forc test will extract those IDs from the received revert code and using the information contained in the ABI JSON provide a rich troubleshooting details. In the above example, in case of a failing test, forc test might display an output similar to the following:
test some_test, "/tests.sw":42
revert code: 8280000000000003
├─ panic message: The provided identity is not an administrator.
├─ panic value: NotAnAdmin(Address(Address(79fa8779bed2f36c3581d01c79df8da45eee09fac1fd76a5a656e16326317ef0)))
├─ panicked: in auth_package::admin_access::only_admin
│ └─ at auth_package@1.2.3, src/admin_access.sw:13:9
└─ backtrace: called in guards_package::preconditions::check_admin
└─ at guards_package@0.1.0, src/preconditions.sw:4:9
called in guards_package::preconditions::check_access_rights
└─ at guards_package@0.1.0, src/preconditions.sw:23:13
called in guards_package::preconditions::check_preconditions
└─ at guards_package@0.1.0, src/preconditions.sw:57:9
called in <Contract as Funds>::transfer_funds
└─ at funds_contract@0.2.5, src/main.sw:22:9
Configuring the Backtrace Content
In Default Builds
Backtracing comes with a minimal on-chain cost, in terms of the bytecode size and gas usage. To additionally allow you to opt-in even for this minimal cost, backtracing is configurable via dedicated backtrace build option, with different default values for debug and release builds.
To explain this build option, let us use the following example. We will have five functions named first, second, ..., fifth that call each other sequentially, and the function fifth finally calling a failing assert_eq that panics.
In the default debug build, the output of a failing forc test will look similar to this (package names and code locations are omitted for brevity):
test some_test, "test.sw":42
revert code: 8280000000000003
├─ panic message: The provided `expected` and `actual` values are not equal.
├─ panic value: AssertEq(AssertEq { expected: 42, actual: 43 })
├─ panicked: in std::assert::assert_eq
│ └─ at std@0.99.0, src/assert.sw:80:9
└─ backtrace: called in fifth
└─ at ...
called in fourth
└─ at ...
called in third
└─ at ...
called in second
└─ at ...
called in first
└─ at ...
In the default release build, the backtrace output will contain only the immediate call of the assert_eq that happens in the fifth:
...
├─ panicked: in std::assert::assert_eq
│ └─ at std@0.99.0, src/assert.sw:80:9
└─ backtrace: called in fifth
└─ at ...
Where this difference in backtrace in debug and release build is coming from? In other words, how the compiler knows which functions to include into backtrace in different build profiles?
The backtrace can be directly influenced by using the #[trace] attribute in your code. Similarly to the #[inline] attribute, the #[trace] attribute can be used on all functions that have implementations. Same like #[inline], it also comes with two arguments, always and never.
The #[trace(always)] instructs the compiler to include the calls of annotated functions in the backtrace in default release builds. This attribute should be used to annotate guard functions, like, e.g., assert, assert_eq, require, and only_owner, or methods like Option::unwrap. When such functions panic, we are actually interested in the places in code in which a failing call happens. E.g., having #[trace(always)] on the assert function helps us to see which actual assert call has failed.
By default, release builds will include in the backtrace only the calls to functions annotated with #[trace(always)]. This minimizes the anyhow low on-chain cost of backtrace calculation only to function calls of those function, giving almost a zero on-chain impact while still providing a valuable troubleshooting information.
By using #[trace(never)] you can instruct the compiler not to include a function in a backtrace, even not in a default debug build, which otherwise includes all the panicking calls into backtrace. This is useful, considering that the backtrace will be limited to five functions only. If an intermediate function call can be easily deducted, it might be worth not having it in the backtrace.
E.g., let's assume that the functions fourth and second are annotated with #[trace(never)]. The default debug build would then output the following backtrace:
...
├─ panicked: in std::assert::assert_eq
│ └─ at std@0.99.0, src/assert.sw:80:9
└─ backtrace: called in fifth
└─ at ...
called in third
└─ at ...
called in first
└─ at ...
In Custom Builds
To change this default behavior, use the backtrace build option. The possible values for the backtrace build option, and their meanings are given in the below table.
| Value | Meaning |
|---|---|
| all | Backtrace all function calls, even of functions annotated with #[trace(never)]. |
| all_except_never | Backtrace all function calls, except those of functions annotated with #[trace(never)]. This is the default value for debug builds. |
| only_always | Backtrace only calls of functions annotated with #[trace(always)]. This is the default value for release builds. |
| none | Do not backtrace any function calls. Use this option only if you need to fully remove the on-chain cost of backtracing. Considering how negligible the cost is, this will very likely never be needed. |
To learn more about custom builds, see The [build-profile.*] Section.