# Solidity 课程 12: 异常

By [Novar](https://paragraph.com/@novar) · 2022-06-07

---

`solidity`一个错误将撤消在交易期间对状态所做的所有改变。有三种抛出异常的方法：`error`，`require`和`assert`。

### 1、Error、revert

`Error`是`solidity` 0.8版本新加的内容，方便且高效（省gas）的向用户解释操作失败的原因。人们可以在`contract`之外定义异常。

下面，我们定义一个`TransferNotOwner`异常，当用户不是代币`owner`的时候尝试转账，会抛出错误：

    error TransferNotOwner(); // 自定义errorfunction
    
    transferOwner1(uint256 tokenId, address newOwner) public {
            if(_owners[tokenId] != msg.sender){
                revert TransferNotOwner();
            }
            _owners[tokenId] = newOwner;
    }
    

注意：在执行当中，**error 必须搭配 revert 命令使用**。

    // custom error
    error InsufficientBalance(uint balance, uint withdrawAmount);
    
    function testCustomError(uint _withdrawAmount) public view {
        uint bal = address(this).balance;
        if (bal < _withdrawAmount) {
            revert InsufficientBalance({balance: bal, withdrawAmount: _withdrawAmount});
        }
    }
    

我们也可以直接使用**revert**：

        function testRevert(uint _i) public pure {
            // Revert is useful when the condition to check is complex.
            // This code does the exact same thing as the example above
            if (_i <= 10) {
                revert("Input must be greater than 10");
            }
        }
    

### 2、Require

`require`命令是`solidity` 0.8 版本之前抛出异常的常用方法，目前很多主流合约仍然还在使用它。

他很好用，唯一的缺点就是`gas`**随着描述异常的字符串长度增加**，比`error`命令要高。使用方法：`require(检查条件，”异常的描述”)`，当`检查条件`不成立的时候，就会抛出异常。

我们用`require`命令重写一下上面的`transferOwner`函数：

    function transferOwner2(uint256 tokenId, address newOwner) public {
            require(_owners[tokenId] == msg.sender, "Transfer Not Owner");
            _owners[tokenId] = newOwner;
    }
    

        function testRequire(uint _i) public pure {
            // Require should be used to validate conditions such as:
            // - inputs
            // - conditions before execution
            // - return values from calls to other functions
            require(_i > 10, "Input must be greater than 10");
        }
    

### 3、Assert

`assert`应该只用于调试，因为他不能解释抛出异常的原因（比`require`少个字符串）。他的用法很简单，`assert(检查条件）`，当`检查条件`不成立的时候，就会抛出异常。

我们用`assert`命令重写一下上面的`transferOwner`函数：

    function transferOwner3(uint256 tokenId, address newOwner) public {
            assert(_owners[tokenId] == msg.sender);
            _owners[tokenId] = newOwner;
    }
    
    function testAssert() public view {
            // Assert should only be used to test for internal errors,
            // and to check invariants.
    
            // Here we assert that num is always equal to 0
            // since it is impossible to update the value of num
            assert(num == 0);
    }
    

### 4、三种方法的gas比较

我们比较一下三种抛出异常的gas消耗，方法很简单，部署合约，分别运行写的`transferOwner`函数的三个版本。

1.  `error`方法gas消耗：24445
    
2.  `require`方法gas消耗：24743
    
3.  `assert`方法gas消耗：24446
    

我们可以看到，`error`方法gas cost最少，其次是`assert`，`require`方法消耗gas最多。因此，`error`是最佳的，既可以告知用户抛出异常的原因，又能省gas。

### 5、统计产生异常的部分原因

下列情况将会产生一个 `assert` 式异常：

1.  如果你访问数组的索引太大或为负数（例如 `x[i]` 其中 `i >= x.length` 或 `i < 0`）。
    
2.  如果你访问固定长度 `bytesN` 的索引太大或为负数。
    
3.  如果你用零当除数做除法或模运算（例如 `5 / 0` 或 `23 % 0` ）。
    
4.  如果你移位负数位。
    
5.  如果你将一个太大或负数值转换为一个枚举类型。
    
6.  如果你调用内部函数类型的零初始化变量。
    
7.  如果你调用 `assert` 的参数（表达式）最终结算为 false。
    

下列情况将会产生一个 `require` 式异常：

1.  调用 `throw` 。
    
2.  如果你调用 `require` 的参数（表达式）最终结算为 `false` 。
    
3.  如果你通过消息调用调用某个函数，但该函数没有正确结束（它耗尽了 gas，没有匹配函数，或者本身抛出一个异常），上述函数不包括低级别的操作 `call` ， `send` ， `delegatecall` 或者 `callcode` 。低级操作不会抛出异常，而通过返回 `false` 来指示失败。
    
4.  如果你使用 `new` 关键字创建合约，但合约没有正确创建（请参阅上条有关”未正确完成“的定义）。
    
5.  如果你对不包含代码的合约执行外部函数调用。
    
6.  如果你的合约通过一个没有 `payable` 修饰符的公有函数（包括构造函数和 fallback 函数）接收 Ether。
    
7.  如果你的合约通过公有 getter 函数接收 Ether 。
    
8.  如果 `.transfer()` 失败。
    

### 总结

这节学习了处理异常的3种方法，revert（error）、require 和 assert，其中 error 最合适，既能省gas，又可以知道抛出的异常。

**Code：**

[https://github.com/Luca-Hsu/SuperSolidity](https://github.com/Luca-Hsu/SuperSolidity)

**Reference:**

[https://solidity-cn.readthedocs.io/zh/develop/control-structures.html#assert-require-revert-and-exceptions](https://solidity-cn.readthedocs.io/zh/develop/control-structures.html#assert-require-revert-and-exceptions)

[https://mirror.xyz/ninjak.eth/XhhLu7PV1cAhOp9\_m-dk9OoTj7offC7DkYYgsV3e31I](https://mirror.xyz/ninjak.eth/XhhLu7PV1cAhOp9_m-dk9OoTj7offC7DkYYgsV3e31I)

[https://solidity-by-example.org/error](https://solidity-by-example.org/error)

---

*Originally published on [Novar](https://paragraph.com/@novar/solidity-12)*
