Clever Errors
Clever errors are a feature that allows for more informative error messages when an assertion fails
or an abort is raised. They are a source feature and compile to a u64
abort code value that
contains the information needed to access the line number, constant name, and constant value given
the clever error code and the module that the clever error constant was declared in. Because of this
compilation, post-processing is required to go from the u64
abort code value to a human-readable
error message. The post-processing is automatically performed by the Sui GraphQL server, as well as
the Sui CLI. If you want to manually decode a clever abort code, you can use the process outlined in
Inflating Clever Abort Codes to do so.
Clever errors include source line information amongst other data. Because of this their value may change due to any changes in the source file (e.g., due to auto-formatting, adding a new module member, or adding a newline).
Clever Abort Codes
Clever abort codes allow you to use non-u64 constants as abort codes as long as the constants are
annotated with the #[error]
attribute. They can be used both in assertions, and as codes to
abort
.
module 0x42::a_module;
#[error]
const EIsThree: vector<u8> = b"The value is three";
// Will abort with `EIsThree` if `x` is 3
public fun double_except_three(x: u64): u64 {
assert!(x != 3, EIsThree);
x * x
}
// Will always abort with `EIsThree`
public fun clever_abort() {
abort EIsThree
}
In this example, the EIsThree
constant is a vector<u8>
, which is not a u64
. However, the
#[error]
attribute allows the constant to be used as an abort code, and will at runtime produce a
u64
abort code value that holds:
- A set tag-bit that indicates that the abort code is a clever abort code.
- The line number of where the abort occured in the source file (e.g., 7).
- The index in the module's identifier table for the constant's name (e.g.,
EIsThree
). - The index of the constant's value in the module's constant table (e.g.,
b"The value is three"
).
In hex, if double_except_three(3)
is called, it will abort with a u64
abort code as follows:
0x8000_0007_0001_0000
^ ^ ^ ^
| | | |
| | | |
| | | +-- Constant value index = 0 (b"The value is three")
| | +-- Constant name index = 1 (EIsThree)
| +-- Line number = 7 (line of the assertion)
+-- Tag bit = 0b1000_0000_0000_0000
And could be rendered as a human-readable error message as (e.g.)
Error from '0x42::a_module::double_except_three' (line 7), abort 'EIsThree': "The value is three"
The exact formatting of this message may vary depending on the tooling used to decode the clever
error however all of the information needed to generate a human-readable error message like the
above is present in the u64
abort code when coupled with the module where the error occurred.
Clever abort code values do not need to be a
vector<u8>
-- it can be any valid constant type in Move.
Assertions with no Abort Codes
Assertions and abort
statements without an abort code will automatically derive an abort code from
the source line number and will be encoded in the clever error format with the constant name and
constant value information will be filled with sentinel values of 0xffff
each. E.g.,
module 0x42::a_module;
#[test]
fun assert_false(x: bool) {
assert!(false);
}
#[test]
fun abort_no_code() {
abort
}
Both of these will produce a u64
abort code value that holds:
- A set tag-bit that indicates that the abort code is a clever abort code.
- The line number of where the abort occured in the source file (e.g., 6).
- A sentinel value of
0xffff
for the index into the module's identifier table for the constant's name. - A sentinel value of
0xffff
for the index of the constant's value in the module's constant table.
In hex, if assert_false(3)
is called, it will abort with a u64
abort code as follows:
0x8000_0004_ffff_ffff
^ ^ ^ ^
| | | |
| | | |
| | | +-- Constant value index = 0xffff (sentinel value)
| | +-- Constant name index = 0xffff (sentinel value)
| +-- Line number = 4 (linke of the assertion)
+-- Tag bit = 0b1000_0000_0000_0000
Clever Errors and Macros
The line number information in clever abort codes are derived from the source file at the location where the abort occurs. In particular, for a function this will be the line number within in the function, however for macros, this will be the location where the macro is invoked. This can be quite useful when writing macros as it provides a way for users to use macros that may raise abort conditions and still get useful error messages.
module 0x42::macro_exporter;
public macro fun assert_false() {
assert!(false);
}
public macro fun abort_always() {
abort
}
public fun assert_false_fun() {
assert!(false); // Will always abort with the line number of this invocation
}
public fun abort_always_fun() {
abort // Will always abort with the line number of this invocation
}
Then in a module that uses these macros:
module 0x42::user_module;
use 0x42::macro_exporter::{
assert_false,
abort_always,
assert_false_fun,
abort_always_fun
};
fun invoke_assert_false() {
assert_false!(); // Will abort with the line number of this invocation
}
fun invoke_abort_always() {
abort_always!(); // Will abort with the line number of this invocation
}
fun invoke_assert_false_fun() {
assert_false_fun(); // Will abort with the line number of the assertion in `assert_false_fun`
}
fun invoke_abort_always_fun() {
abort_always_fun(); // Will abort with the line number of the `abort` in `abort_always_fun`
}
Inflating Clever Abort Codes
Precisely, the layout of a clever abort code is as follows:
|<tagbit>|<reserved>|<source line number>|<module identifier index>|<module constant index>|
+--------+----------+--------------------+-------------------------+-----------------------+
| 1-bit | 15-bits | 16-bits | 16-bits | 16-bits |
Note that the Move abort will come with some additional information -- importantly in our case the module where the error occurred. This is important because the identifier index, and constant index are relative to the module's identifier and constant tables (if not set the sentinel values).
To decode a clever abort code, you will need to know the module where the error occurred if either the identifier index or constant index are not set to the sentinel value of
0xffff
.
In pseudo-code, you can decode a clever abort code as follows:
#![allow(unused)] fn main() { // Information available in the MoveAbort let clever_abort_code: u64 = ...; let (package_id, module_name): (PackageStorageId, ModuleName) = ...; let is_clever_abort = (clever_abort_code & 0x8000_0000_0000_0000) != 0; if is_clever_abort { // Get line number, identifier index, and constant index // Identifier and constant index are sentinel values if set to '0xffff' let line_number = ((clever_abort_code & 0x0000_ffff_0000_0000) >> 32) as u16; let identifier_index = ((clever_abort_code & 0x0000_0000_ffff_0000) >> 16) as u16; let constant_index = ((clever_abort_code & 0x0000_0000_0000_ffff)) as u16; // Print the line error message print!("Error from '{}::{}' (line {})", package_id, module_name, line_number); // No need to print anything or load the module if both are sentinel values if identifier_index == 0xffff && constant_index == 0xffff { return; } // Only needed if constant name and value are not 0xffff let module: CompiledModule = fetch_module(package_id, module_name); // Print the constant name (if any) if identifier_index != 0xffff { let constant_name = module.get_identifier_at_table_index(identifier_index); print!(", '{}'", constant_name); } // Print the constant value (if any) if constant_index != 0xffff { let constant_value = module.get_constant_at_table_index(constant_index).deserialize_on_constant_type().to_string(); print!(": {}", constant_value); } return; } }