ERROR HANDLER

Require In solidity

in solidity we use require to verify user's input, the EVM will revert if x is greater or equals to 100. ( Revert means the state won't be changed.)

function foobar(uint256 x) public {
    require(x < 100, "I'm not happy with the number you picked");
  // rest of the function logic
}

Code

create a new program, you can also get the source code here: day_4

anchor new day_4

update the code of file:day_4/src/lib.rs to:

use anchor_lang::prelude::*;

declare_id!("5q5xqHhCFFtLpBSL7byhLihBdcjXZYDqjyfB8QZE4BGR");

#[program]
pub mod day_4 {
    use super::*;

    // 1. this function will return an error, but the tx won't revert, differ from solidity!
    pub fn limit_range(_ctx: Context<LimitRange>, a: u64) -> Result<()> {
        if a < 10 {
            return err!(MyError::AisTooSmall);
        }
        if a > 100 {
            return err!(MyError::AisTooBig);
        }
        msg!("Result = {}", a);
        Ok(())
    }

    // 2. the require! macro is a shortcut for the above function, won't revert the tx neither
    pub fn limit_range_require(_ctx_then: Context<LimitRange>, a: u64) -> Result<()> {
        require!(a >= 10, MyError::AisTooSmall);
        require!(a <= 100, MyError::AisTooBig);
        msg!("Result = {}", a);
        Ok(())
    }

    // 3. won't revert and the msg! macro will not print, cos it return err!() instead of Ok(())
    pub fn funcError(_ctx: Context<LimitRange>) -> Result<()> {
        msg!("Will this print when return err!() ?");
        return err!(MyError::AlwaysErrors);
        // output: this msg will not print
    }

    // 4. will print the msg! macro, cos it return Ok(())
    pub fn funcOK(_ctx: Context<LimitRange>) -> Result<()> {
        msg!("Will this print when return OK() ?");
        // return err!(MyError::AlwaysErrors);
        return Ok(());

        // output: this msg will print
    }
}

#[derive(Accounts)]
pub struct LimitRange {}

#[error_code]
pub enum MyError {
    #[msg("a is too small")]
    AisTooSmall,
    #[msg("a is too big")]
    AisTooBig,
    #[msg("Always errors")]
    AlwaysErrors,
}

Test

create test file for day_4

import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Day4 } from "../target/types/day_4";
import { assert } from "chai";

describe("day_4", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.Day4 as Program<Day4>;

  it.only("Input test too small", async () => {
    try {
      const tx = await program.methods.limitRange(new anchor.BN(9)).rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      // console.log(_err);
      assert.isTrue(_err instanceof anchor.AnchorError);

      const err: anchor.AnchorError = _err;
      const errMsg = "a is too small";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("error number:", err.error.errorCode.number);
    }
  })

  it.only("Input test too big", async () => {
    try {
      const tx = await program.methods.limitRange(new anchor.BN(101)).rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      assert.isTrue(_err instanceof anchor.AnchorError);
      const err: anchor.AnchorError = _err;
      const errMsg =
        "a is too big";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("Error number:", err.error.errorCode.number);
    }
  })

  it.only("Input test require, too small", async () => {
    try {
      const tx = await program.methods.limitRangeRequire(new anchor.BN(9)).rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      // console.log(_err);
      assert.isTrue(_err instanceof anchor.AnchorError);

      const err: anchor.AnchorError = _err;
      const errMsg = "a is too small";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("error number:", err.error.errorCode.number);
    }
  })

  it.only("Input test require, too big", async () => {
    try {
      const tx = await program.methods.limitRangeRequire(new anchor.BN(101)).rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      assert.isTrue(_err instanceof anchor.AnchorError);
      const err: anchor.AnchorError = _err;
      const errMsg =
        "a is too big";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("Error number:", err.error.errorCode.number);
    }
  })

  it.only("Error test funcError", async () => {
    try {
      const tx = await program.methods.funcError().rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      assert.isTrue(_err instanceof anchor.AnchorError);
      const err: anchor.AnchorError = _err;
      const errMsg =
        "Always errors";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("Error number:", err.error.errorCode.number);
    }
  });

  it.only("Error test funcOK", async () => {
    try {
      const tx = await program.methods.funcOk().rpc();
      console.log("Your transaction signature", tx);
    } catch (_err) {
      assert.isTrue(_err instanceof anchor.AnchorError);
      const err: anchor.AnchorError = _err;
      const errMsg =
        "Always errors";
      assert.strictEqual(err.error.errorMessage, errMsg);
      console.log("Error number:", err.error.errorCode.number);
    }
  });
});

Test:

anchor deploy -p day_4
anchor test --skip-local-validator --skip-deploy --skip-build

It it fails to test, delete target folder and run:

anchor test --skip-local-validator

Result

Logs and Test results:

image-20240721134338543

Key Takeaways

  1. error! and require!are the same, and both of them won't revert the tx, this is quite to solidity, be sure of awareness.
  2. we can fetch the revert under try-catch block.
  3. the msg!won't be printed if the function return an error.