Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardize errors #1165

Open
wants to merge 89 commits into
base: master
Choose a base branch
from

Conversation

PolyProgrammist
Copy link
Contributor

@PolyProgrammist PolyProgrammist commented Apr 8, 2024

Closes #936

The following information comes from docs:

Panic behaviour

If return type is Result<OkType, ErrType> and, the function is not marked with #[handle_result] and
ErrType struct implements near_sdk::ContractErrorTrait (i.e. used with #[near_sdk::contract_error])

When the contract call happens:

  • In case Result value is Err, panic is called and state is not written.
    As soon as ErrType implements ContractErrorTrait, it is returned as a well-defined structure.
    If the error struct does not implement ContractErrorTrait, the code should not compile.
  • In case #[persist_on_error] is used on method, panic is not called.
    And the contract state is written safely.
    But the extra <method_name>_error method is generated.
    And this method is called in a new Promise.
    This method effectively panics with structured error.

Example:

#[contract_error]
pub struct MyError;

// if Ok() is returned, everything ok, otherwise panic with well-structured error
pub fn foo(&mut self) -> Result<u64, MyError>;
// Write state safely anyway.
// if Ok() is returned, just return. Otherwise call new Promise which will panic with well-structured error.
#[persist_on_error]
pub fn foo(&mut self) -> Result<u64, MyError>;

contract_error macro

This attribute macro is used on a struct or enum to generate the necessary code for an error
returned from a contract method call.

Example:

For the error:

#[contract_error]
pub struct MyError {
    field1: String,
    field2: String,
}

And the function:

pub fn my_method(&self) -> Result<(), MyError>;

The error is serialized as a JSON object with the following structure:

{
"error": {
    // this name can be "SDK_CONTRACT_ERROR" and "CUSTOM_CONTRACT_ERROR"
    // To generate "SDK_CONTRACT_ERROR", use sdk attribute `#[contract_error(sdk)]`.
    // Otherwise, it will generate "CUSTOM_CONTRACT_ERROR"
    "name": "CUSTOM_CONTRACT_ERROR",
    "cause": {
        // this name is the name of error struct
        "name": "MyError",
        "info": {
            /// fields of the error struct
            "field1": "value1",
            "field2": "value2"
        }
}
}

Note: you can assign any error defined like that to BaseError:

let base_error: BaseError = MyError { field1: "value1".to_string(), field2: "value2".to_string() }.into();

Use inside_nearsdk attribute (#[contract_error(inside_nearsdk)]) if the error struct is defined inside near-sdk.
Don't use if it is defined outside.

Internally, it makes error struct to:

  • implement near_sdk::ContractErrorTrait so that it becomes correct error
    which can be returned from contract method with defined structure.
  • implement From<ErrorStruct> for near_sdk::BaseError as a polymorphic solution
  • implement From<ErrorStruct> for String to convert the error to a string

unwrap_on_err macro

Helper macro to unwrap an Option or Result, returning an error if None or Err.

  • If you have an option you would like to unwrap, you use unwrap_or_err! on it and
    provide an error that will be returned from the function in case the option value is None

  • If you have a result you would like to unwrap, you use unwrap_or_err! on it and
    the error will be returned from the function in case the result is an Err

Examples

use near_sdk::unwrap_or_err;
use near_sdk::errors::ContractError;

# fn main() -> Result<u64, ContractError> {
let error = ContractError("Some error");

let option_some: Option<u64> = Some(5);
let option_none: Option<u64> = None;

let result_ok: Result<u64, ContractError> = Ok(5);
let result_err: Result<u64, ContractError> = Err(error);

let option_success: u64 = unwrap_or_err!(option_some, error); // option_success == 5
let option_error: u64 = unwrap_or_err!(option_none, error); // error is returned from main

let result_success: u64 = unwrap_or_err!(result_ok); // result_success == 5
let result_error: u64 = unwrap_or_err!(result_err); // error is returned from main

Ok(0)
# }

require_on_err macro

Helper macro to create assertions that will return an error.

This macro can be used similarly to [require!] but will return an error instead of panicking.

Returns Err(near_sdk::errors::RequireFailed) unless error message provided

Examples

use near_sdk::require_or_err;
use near_sdk::errors::ContractError;

# fn main() {
let a = 2;
require_or_err!(a > 0);
require_or_err!("test" != "other", near_sdk::error::Error("Some custom error message if false"));
# }

Examples

// Find all our documentation at https://docs.near.org
use near_sdk::contract_error;
use near_sdk::near;
use near_sdk::BaseError;

#[contract_error]
pub enum MyErrorEnum {
    X,
}

#[contract_error(sdk)]
pub struct MyErrorStruct {
    x: u32,
}

#[near(contract_state)]
#[derive(Default)]
pub struct Contract {
    value: u32,
}

#[near]
impl Contract {
    #[init]
    pub fn new() -> Self {
        Self { value: 0 }
    }

    // Examples of RPC response for function call:
    // is_error = false
    // --- Result -------------------------
    // 1
    // ------------------------------------
    // (changes value from 0 to 1)
    //
    // is_error = true
    // Failed transaction
    // Error:
    // 0: Error: An error occurred during a `FunctionCall` Action, parameter is debug message.
    // ExecutionError("Smart contract panicked: error in inc_handle_result")
    // (does not change value)
    #[handle_result]
    pub fn inc_handle_result(&mut self, is_error: bool) -> Result<u32, &'static str> {
        self.value += 1;
        if is_error {
            Err("error in inc_handle_result")
        } else {
            Ok(self.value)
        }
    }

    // Examples of RPC response for function call:
    // is_error = false
    // --- Result -------------------------
    // 2
    // ------------------------------------
    // (changes value from 1 to 2)
    //
    // is_error = true
    // Failed transaction
    // Error:
    // 0: Error: An error occurred during a `FunctionCall` Action, parameter is debug message.
    // ExecutionError("Smart contract panicked: {\"error\":{\"error_type\":\"error_handling::MyErrorEnum\",\"value\":\"X\"}}")
    // (changes value from 2 to 3)
    #[persist_on_error]
    pub fn inc_persist_on_err(&mut self, is_error: bool) -> Result<u32, MyErrorEnum> {
        self.value += 1;
        if is_error {
            Err(MyErrorEnum::X)
        } else {
            Ok(self.value)
        }
    }

    // Examples of RPC response for function call:
    // is_error = false
    // --- Result -------------------------
    // 4
    // ------------------------------------
    // (changes value from 3 to 4)
    //
    // is_error = true
    // Failed transaction
    // Error:
    // 0: Error: An error occurred during a `FunctionCall` Action, parameter is debug message.
    //  ExecutionError("Smart contract panicked: {\"error\":{\"error_type\":\"error_handling::MyErrorStruct\",\"value\":{\"x\":5}}}")
    // (does not change value)
    pub fn inc_just_result(&mut self, is_error: bool) -> Result<u32, MyErrorStruct> {
        self.value += 1;
        if is_error {
            Err(MyErrorStruct { x: 5 })
        } else {
            Ok(self.value)
        }
    }

    // Examples of RPC response for function call:
    // is_error = false
    // --- Result -------------------------
    // 5
    // ------------------------------------
    // (changes value from 4 to 5)
    //
    // is_error = true
    // Failed transaction
    // Error:
    // 0: Error: An error occurred during a `FunctionCall` Action, parameter is debug message.
    //  ExecutionError("Smart contract panicked: Error")
    // (does not change value)
    pub fn inc_just_simple(&mut self, is_error: bool) -> u32 {
        self.value += 1;
        if is_error {
            ::near_sdk::env::panic_str("Error");
        } else {
            self.value
        }
    }

    // Examples of RPC response for function call:
    // is_error = false
    // --- Result -------------------------
    // 6
    // ------------------------------------
    // (changes value from 5 to 6)
    //
    // is_error = true
    // Failed transaction
    // Error:
    // 0: Error: An error occurred during a `FunctionCall` Action, parameter is debug message.
    //  ExecutionError("Smart contract panicked: {\\\"error\\\":{\\\"cause\\\":{\\\"info\\\":{\\\"error\\\":{\\\"x\\\":5}},\\\"name\\\":\\\"near_sdk::utils::contract_error::BaseError\\\"},\\\"name\\\":\\\"CUSTOM_CONTRACT_ERROR\\\"}}")
    // (does not change value)
    pub fn inc_base_error(&mut self, is_error: bool) -> Result<u32, BaseError> {
        self.value += 1;
        if is_error {
            Err(MyErrorStruct { x: 5 }.into())
        } else {
            Ok(self.value)
        }
    }

    // Does not compile as u64 is not marked with contract_error
    // > the trait `ContractErrorTrait` is not implemented for `u64`
    // pub fn inc_incorrect_result_type(&mut self, is_error: bool) -> Result<u32, u64> {
    //     self.value += 1;
    //     if is_error {
    //         Err(0)
    //     } else {
    //         Ok(self.value)
    //     }
    // }

    pub fn get_value(&self) -> u32 {
        self.value
    }
}

to keep the legacy behavior, mark the method with #[handle_result] and make \
it return Result<Result<T, E>, near_sdk::Abort>.",
))
Ok(ReturnKind::HandlesResultImplicit(crate::StatusResult {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to mention this change: from some time using Result without #[handle_result] became deprecated. So introducing it again may break contracts that use that old sdk and move to the new version. Please let me know if this change should be reconsidered.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for flagging it. Could you, please, dig into the git history/blame to learn why it was decided to use an explicit attribute instead of handling Result types?

This change won't break previously compilable contracts. We only change the behavior for the contracts that could not have been compiled before or very old contracts which we won't migrate to this new SDK anyway.

Copy link
Contributor Author

@PolyProgrammist PolyProgrammist May 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was initially called #[return_result] instead of #[handle_result] - change applied

Here is the first issue about this macro and comment by austinabell. According to the comment I believe it was decided to use explicit attribute so that it does not accidentally break existing contracts. So the macro was introduced in March 2022 along with forbidding to use Result without macro a bit later.

@PolyProgrammist PolyProgrammist marked this pull request as ready for review May 31, 2024 10:09
@PolyProgrammist PolyProgrammist requested a review from frol May 31, 2024 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Standardized errors
2 participants