Tutorial·8 min का पठन·Solingo द्वारा

Solidity में Custom Errors: सस्ते और स्पष्ट reverts

Custom errors gas बचाते हैं, bytecode छोटा करते हैं और failures को self-documenting बनाते हैं। यहाँ देखें कि इन्हें कैसे define करें, arguments के साथ revert करें, Foundry में decode करें, और कब एक सादा require बेहतर रहता है।

# Solidity में Custom Errors: सस्ते और स्पष्ट reverts

आप जो भी contract लिखेंगे वह कभी न कभी revert करेगा। असली सवाल यह है कि कैसे। सालों तक default जवाब था require(condition, "कोई string"), और यह string चुपचाप हर deployment पर gas खर्च करवाती थी और ऐसा bytecode जोड़ती थी जिसकी आपको शायद ही जरूरत होती थी। Custom errors, जो Solidity 0.8.4 से उपलब्ध हैं, failure बताने का एक साफ, सस्ता और ज्यादा expressive तरीका देते हैं। यह guide इन्हें define करने, arguments के साथ revert करने, error selectors समझने, Foundry tests में decode करने, और उन हालातों को कवर करती है जहाँ एक सादा require अब भी सही चुनाव है।

require strings की समस्या

एक revert reason string contract के bytecode में store होती है और runtime पर ABI-encode होती है। यह जाना-पहचाना pattern देखिए:

function withdraw(uint256 amount) external {

require(amount <= balances[msg.sender], "Insufficient balance");

// ...

}

यह literal "Insufficient balance" हमेशा के लिए आपके deployed bytecode में रहता है। हर string जो आप जोड़ते हैं, deployment cost बढ़ाती है। Runtime पर, जब check fail होता है, EVM उस string को Error(string) selector में ABI-encode करता है, जो एक compact custom error encode करने से ज्यादा महंगा है। आपके messages जितने लंबे और जितने ज्यादा होंगे, एक असली codebase में यह उतना ही जुड़ता जाएगा।

Custom errors define करना और उनके साथ revert करना

एक custom error error keyword से declare होती है और revert statement से trigger होती है:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.4;

contract Vault {

mapping(address => uint256) public balances;

error InsufficientBalance(uint256 requested, uint256 available);

error ZeroAmount();

function withdraw(uint256 amount) external {

if (amount == 0) {

revert ZeroAmount();

}

uint256 bal = balances[msg.sender];

if (amount > bal) {

revert InsufficientBalance(amount, bal);

}

balances[msg.sender] = bal - amount;

(bool ok, ) = msg.sender.call{value: amount}("");

require(ok, "transfer failed");

}

}

ध्यान दीजिए हमें क्या मिला। InsufficientBalance(amount, bal) सिर्फ यह नहीं कहती कि कुछ गलत हुआ, बल्कि caller को ठीक-ठीक बताती है कि कितना माँगा गया और कितना available था। ये arguments runtime पर encode होते हैं और off-chain tooling को दिखते हैं, लेकिन parameter names और descriptive text bytecode में string की तरह fix नहीं होते।

Custom errors इस तरह declare हो सकती हैं:

  • किसी contract के अंदर, उसी contract तक सीमित scope के साथ।
  • File level पर, ताकि कई contracts और libraries इन्हें share कर सकें।
  • किसी interface या library के अंदर, फिर ILib.SomeError की तरह reference की जाएँ।

File-level और shared errors एक project भर में failure semantics को बिना definitions दोहराए consistent रखने का अच्छा तरीका हैं।

Error selectors: असल में wire पर क्या जाता है

जब आप किसी custom error के साथ revert करते हैं, तो EVM ABI-encoded data लौटाता है जो एक 4-byte selector से शुरू होता है, ठीक एक function call की तरह। Selector, error signature के keccak256 hash के पहले चार bytes होते हैं। InsufficientBalance(uint256,uint256) के लिए:

bytes4 selector = bytes4(keccak256("InsufficientBalance(uint256,uint256)"));

// modern Solidity में समतुल्य रूप से:

bytes4 selector2 = Vault.InsufficientBalance.selector;

पूरा revert payload, selector के बाद ABI-encoded arguments होता है, जो abi.encodeWithSelector(Vault.InsufficientBalance.selector, amount, bal) बनाता। यह built-in Error(string) और Panic(uint256) errors के अंदरूनी काम करने के तरीके जैसा ही है। यह 4-byte prefix ही ठीक वजह है कि on-chain footprint इतना छोटा होता है: एक reason string को पूरा message encode करना पड़ता है, जबकि एक custom error सिर्फ एक fixed selector और आपके चुने हुए data को encode करती है।

चूँकि selectors signature से बनते हैं, एक ही नाम लेकिन अलग parameter types वाली दो errors अलग errors होती हैं। किसी error का नाम बदलना या उसके parameters बदलना selector बदल देता है, जो मायने रखता है अगर external tools उस पर match करते हों।

किसी दूसरे contract में custom error पकड़ना

आप एक typed catch से किसी खास error type पर react कर सकते हैं:

interface IVault {

error InsufficientBalance(uint256 requested, uint256 available);

function withdraw(uint256 amount) external;

}

contract Caller {

event Shortfall(uint256 requested, uint256 available);

function tryWithdraw(IVault vault, uint256 amount) external {

try vault.withdraw(amount) {

// success path

} catch (bytes memory reason) {

bytes4 sel = bytes4(reason);

if (sel == IVault.InsufficientBalance.selector) {

(uint256 requested, uint256 available) =

abi.decode(_slice(reason), (uint256, uint256));

emit Shortfall(requested, available);

} else {

revert("unknown failure");

}

}

}

}

व्यवहार में, आप error पहचानने के लिए पहले 4 bytes पढ़ते हैं, फिर arguments वापस पाने के लिए बाकी bytes (selector के बाद का सब कुछ) को ABI-decode करते हैं। ऊपर दिया _slice helper, abi.decode से पहले शुरुआती selector हटाने का संक्षिप्त रूप है। यह pattern एक calling contract को इस आधार पर फैसले लेने देता है कि call क्यों fail हुई, न कि सिर्फ इस आधार पर कि fail हुई या नहीं।

Foundry tests में custom errors decode करना

Foundry custom errors को first-class support देता है, जिससे इन्हें test करना सुखद हो जाता है। यह assert करने के लिए कि कोई call किसी खास error के साथ revert करती है, vm.expectRevert को error selector या पूरी तरह encoded error के साथ इस्तेमाल करें:

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.13;

import "forge-std/Test.sol";

import "../src/Vault.sol";

contract VaultTest is Test {

Vault vault;

function setUp() public {

vault = new Vault();

}

function test_RevertWhen_AmountIsZero() public {

vm.expectRevert(Vault.ZeroAmount.selector);

vault.withdraw(0);

}

function test_RevertWhen_Insufficient() public {

// error को उसके ठीक arguments के साथ expect करें

vm.expectRevert(

abi.encodeWithSelector(

Vault.InsufficientBalance.selector,

100,

0

)

);

vault.withdraw(100);

}

}

दो स्तर की सख्ती उपलब्ध है:

  • सिर्फ selector पास करें (Vault.ZeroAmount.selector) ताकि केवल error type assert हो।
  • पूरी तरह encoded error को abi.encodeWithSelector(...) के जरिए पास करें ताकि exact argument values भी assert हों।
  • जब किसी अप्रत्याशित revert की वजह से कोई test fail होता है, Foundry trace में आपके लिए custom error decode कर देता है, error name और decoded arguments दिखाते हुए, बशर्ते error definition scope में हो। आप किसी भी run में verbose traces के साथ decoded reverts भी सामने ला सकते हैं:

    forge test -vvvv

    यह decoded custom errors के साथ पूरे call traces print करता है, ताकि एक fail होती InsufficientBalance(100, 0) ठीक वैसी ही पढ़ी जाए, न कि raw hex की तरह।

    कब फिर भी require इस्तेमाल करें

    Custom errors default चुनाव हैं, पर require गायब नहीं हुआ है, और modern Solidity तो require को सीधे एक custom error लेने भी देता है। कुछ दिशानिर्देश:

    • बिना data वाले सरल boolean guards readability के लिए require(cond, "...") ही रह सकते हैं, खासकर छोटे contracts या scripts में जहाँ bytecode size मायने नहीं रखता।
    • Third-party या low-level call results, जैसे ऊपर का require(ok, "transfer failed"), एक छोटी string का आम और बिल्कुल ठीक उपयोग हैं।
    • हाल के compiler versions में आप require(cond, CustomError(arg)) लिख सकते हैं, जिससे require की सहूलियत और एक custom error का gas profile दोनों मिलते हैं।
    • Custom errors इस्तेमाल करें जब भी आप context पास करना चाहें (amounts, addresses, identifiers), जब आपके पास कई अलग failure modes हों, या जब external contracts और tools को failure की वजह पर branch करना हो।

    assert statement एक अलग tool है: यह Panic(uint256) trigger करता है और उन invariants के लिए है जो कभी false नहीं होने चाहिए, input validation के लिए नहीं।

    Solingo पर अभ्यास करें

    यह सब आत्मसात करने का सबसे तेज तरीका है reverts लिखना, उन्हें जानबूझकर तोड़ना और decoded output देखना। app.solingo-blockchain.xyz पर आप custom errors define कर सकते हैं, arguments के साथ revert कर सकते हैं, और interactive exercises में उन पर Foundry-style assertions चला सकते हैं, जहाँ पुराने require string तरीके के मुकाबले gas और bytecode के अंतर साथ-साथ दिखाए जाते हैं। require strings से भरे एक contract को custom errors में बदलकर देखें और deployment cost का फर्क खुद तुलना करें।

    Practice में लगाने के लिए तैयार हैं?

    Solingo पर interactive exercises के साथ इन concepts को apply करें।

    मुफ्त में शुरू करें