WTF Solidity: Manejo de Errores en Solidity — Guía Práctica deerror,requireyassert
【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
导读:本篇文章基于 WTF-Solidity 教程第 15 讲(西语版教程),系统讲解 Solidity 中三种抛出异常的语句:
error、require和assert。通过完整的合约源码、Remix 控制台实测截图与 gas 消耗对比,你将掌握如何在智能合约中正确地校验前置条件、向用户解释失败原因,并在编写合约时做出最省 gas 的异常处理选择。
智能合约一旦部署上链便无法修改,任何未预期的状态都可能造成资金损失。Solidity 提供了多套异常处理机制来保证"失败即回滚(revert)"的原子性语义,但它们在使用方式、可读性与 gas 开销上差异明显。本文以 WTF-Solidity 仓库中Errors合约的实际代码为主线,逐一拆解error、require、assert的语法、适用场景与实测 gas 成本。
Solidity 中的异常:编译期错误与运行期错误
Solidity 的异常可以发生在两个阶段:
- 编译期(compile time):如类型不匹配、语法错误,在编译阶段就被编译器拦截;
- 运行期(runtime):如条件校验失败、算术溢出、调用不存在的函数等,此时交易会整体回滚(revert),所有状态变更被撤销。
本篇聚焦运行期异常。Solidity 0.8之后,语言内置了算术溢出检查,开发者对异常的掌控主要围绕三种语句展开:
error+revert:Solidity0.8.4起引入的自定义错误机制;require(condition, "message"):0.8之前最常用的校验语句;assert(condition):面向内部逻辑断言,不带错误消息。
下面结合仓库中的 Error.sol 源码逐一说明。
使用error自定义错误:省 gas 且信息清晰
声明自定义错误
error是 Solidity0.8.4版本新增的特性,也是当前官方推荐的首选异常方式。它可以在合约内部或外部声明,且可以携带参数,既省 gas 又能向调用者清晰说明失败原因,还方便开发者调试。
在 Error.sol 中,我们在合约外部声明了一个最简单的自定义错误:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.34; // Declaración de error TransferNotOwner error TransferNotOwner();也可以声明一个携带参数的错误,把"谁发起了非法操作"一并抛出,便于链下索引与调试:
error TransferNotOwner(address sender); // error personalizado con parámetro用revert触发错误
在函数体中,error必须配合revert语句使用:当条件不满足时执行revert TransferNotOwner();,交易即刻回滚并携带错误信息。仓库中的transferOwner1()完整实现了这一模式(见 Error.sol):
contract Errors{ // Un mapping que registra el propietario para cada TokenId mapping(uint256 => address) private _owners; // Error : precio de gas 24445 function transferOwner1(uint256 tokenId, address newOwner) public { if(_owners[tokenId] != msg.sender){ revert TransferNotOwner(); // revert TransferNotOwner(msg.sender); } _owners[tokenId] = newOwner; } ... }该函数先校验当前调用者msg.sender是否为该tokenId的 owner;若不是,则抛出TransferNotOwner并回滚;若是,则更新 owner。把revert TransferNotOwner(msg.sender);的注释打开,即可改为携带调用者地址的带参版本。
补充说明:
error携带参数时 gas 会略增(仓库实测约 24660 wei),但仍低于require携带字符串的版本,且信息表达能力更强。
require:经典的条件校验语句
require是 Solidity0.8之前最主流的异常处理方式,至今仍在大量主流合约中使用。其语法为:
require(condition, "mensaje de error");当condition不成立时抛出异常;"mensaje de error"会随异常一起返回给调用方,便于用户理解失败原因。
用require重写上面的转账函数(Error.sol):
// requirer : precio de gas 24743 function transferOwner2(uint256 tokenId, address newOwner) public { require(_owners[tokenId] == msg.sender, "Transfer Not Owner"); _owners[tokenId] = newOwner; }require的优点是简单直观,但存在一个显著缺点:gas 消耗随错误字符串长度的增长而线性上升。因为字符串需要作为 calldata 存储并返回,消息越长,成本越高。这也是error相比require的核心优势之一——自定义错误名以 selector 形式编码,开销与消息长度无关。
assert:面向调试的不变量断言
assert通常用于程序开发阶段的debug:它不携带任何错误消息,因此无法向用户解释失败原因,一般不建议用在面向用户的接口上。
语法如下:
assert(condition);条件不成立即抛出异常。用assert重写转账函数(Error.sol):
// Afirmar : precio de gas 24446 function transferOwner3(uint256 tokenId, address newOwner) public { assert(_owners[tokenId] == msg.sender); _owners[tokenId] = newOwner; }值得注意的版本差异:在 Solidity0.8.0之前的版本中,assert抛出的是panic exception,会消耗掉剩余的全部 gas 且不返还,因此当年被严格限制用于"理论上绝不可能失败"的内部不变量检查;0.8.0之后,assert与其他异常语句一样仅消耗固定开销,但语义上仍被保留用于内部一致性断言(如assert(balance <= totalSupply))。
Remix 实操验证:三种异常的差别一览
在 Remix IDE 中部署Errors合约后(Remix编译器建议使用仓库注释对应的0.8.17版本以获得与本文一致的 gas 数据),依次调用三个函数,传入任意uint256数字与非零地址,即可在控制台看到三种截然不同的反馈(对应 Error.sol 中实现)。
1. 调用transferOwner1()(error方式):控制台抛出异常,并直接显示自定义错误名TransferNotOwner,信息简洁、定位精准。
2. 调用transferOwner2()(require方式):控制台抛出异常,并打印require中定义的字符串消息"Transfer Not Owner",可读性强但成本更高。
3. 调用transferOwner3()(assert方式):控制台仅提示交易回滚,没有任何错误消息,用户无从得知失败原因。
三种方式的输出差异一目了然:error输出错误名、require输出字符串、assert仅回滚无提示。
Gas 对比:究竟哪种方式最省钱
通过 Remix 控制台的Debug按钮可以查看每次函数调用的实际 gas 消耗。使用0.8.17版本编译、以tokenId = 123与非零地址测试(相关数据记录于 15_Errors/Error.sol 与 中文原版教程),三种方式对比结果如下:
| 异常方式 | 调用函数 | gas 消耗(wei) |
|---|---|---|
error(无参数) | transferOwner1 | 24457 |
error(带参数sender) | transferOwner1 | 24660 |
require(带字符串消息) | transferOwner2 | 24755 |
assert(无消息) | transferOwner3 | 24473 |
结论非常明确:error的 gas 最少,其次是assert,require消耗最多。也就是说,error既能向用户说明失败原因,又能节省 gas,是三种方式中的最优解,应在实际开发中优先使用。
需要补充两点注意事项:
- 数值会因测试时间、编译版本不同而略有浮动(例如仓库西语版 Error.sol 注释中的数值为 24445 / 24743 / 24446),但三种方式的相对大小关系始终一致,不影响结论。
require不带消息时反而比assert便宜:不带消息的revert返回空数据,而assert需要返回固定 36 字节的Panic(uint256)错误数据。但二者都无法告知用户失败原因,实际开发中仍推荐使用error。
总结
本讲基于 WTF-Solidity 的Errors合约(Error.sol,中文原版见 15_Errors/Error.sol),介绍了 Solidity 三种抛出异常的方式:
error+revert:Solidity0.8.4新增,支持自定义错误名与参数,gas 最省,是官方推荐的首选方式;require:0.8之前的经典校验语句,带字符串消息可读性好,但 gas 随消息长度线性增长,综合成本最高;assert:面向内部逻辑断言,无错误消息,历史上(0.8之前)会耗尽剩余 gas,仅适合调试与不变量检查。
实际开发建议:面向用户的参数校验与业务逻辑校验优先使用error,内部不变量检查再考虑assert,require可作为兼容旧代码风格的备选。若想完整运行验证,可直接在 Remix 中部署仓库中的 Error.sol,并参考中文原版教程 15_Errors/readme.md 中的补充说明。
【免费下载链接】WTF-SolidityWTF Solidity 极简入门教程,供小白们使用。Now supports English! 官网: https://wtf.academy项目地址: https://gitcode.com/GitHub_Trending/wt/WTF-Solidity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考