🎯 Solidity Events & Error Handling
Your Smart Contract’s Communication System
🌟 The Big Picture
Imagine your smart contract is like a robot chef in a kitchen. The chef does amazing things, but:
- Events = The chef shouting “Order ready!” so waiters know something happened
- Error Handling = The chef checking ingredients before cooking and saying “Sorry, we’re out of eggs!”
Without these, nobody knows what happened, and mistakes go unnoticed!
📢 Part 1: Solidity Events
What Are Events?
Think of events like a megaphone announcement at a stadium.
When something important happens in your contract (someone bought tokens, transferred money, etc.), you want the outside world to know. Events are your way of broadcasting messages that apps can listen to.
// Declaring an event - like setting up your megaphone
event Transfer(
address indexed from,
address indexed to,
uint256 amount
);
// Using the event - making the announcement!
function sendMoney(address to, uint256 amount) public {
// ... transfer logic ...
emit Transfer(msg.sender, to, amount);
}
Why Do We Need Events?
| Without Events 😢 | With Events 😊 |
|---|---|
| Apps must constantly ask “Did anything happen?” | Apps get notified instantly |
| Uses lots of gas checking | Super cheap to emit |
| Hard to track history | Easy to search past events |
The indexed Keyword
Think of it like a library book index!
When you mark a parameter as indexed, you can search for events with that specific value easily.
event Purchase(
address indexed buyer, // Searchable!
uint256 indexed productId, // Searchable!
uint256 price // Not searchable
);
Rule: You can have maximum 3 indexed parameters per event.
graph TD A["Smart Contract"] -->|emit event| B["Blockchain Log"] B -->|indexed params| C["Fast Search Index"] B -->|non-indexed| D["Full Data Storage"] C --> E["Apps Filter by Address"] D --> E E --> F["User Interface Updates"]
📝 Part 2: Event Logging
How Logging Works
When you emit an event, it creates a log entry on the blockchain. These logs are:
- ✅ Cheap (way less gas than storage)
- ✅ Permanent (never deleted)
- ✅ Readable by apps off-chain
- ❌ NOT readable by smart contracts
contract VotingSystem {
event VoteCast(
address indexed voter,
uint256 indexed proposalId,
bool support
);
function vote(uint256 proposalId, bool support)
public
{
// Record the vote logic here...
// Log the vote for history!
emit VoteCast(msg.sender, proposalId, support);
}
}
Real-World Logging Pattern
Here’s a complete example with multiple events:
contract SimpleBank {
event Deposit(
address indexed user,
uint256 amount
);
event Withdrawal(
address indexed user,
uint256 amount
);
mapping(address => uint256) public balances;
function deposit() public payable {
balances[msg.sender] += msg.value;
emit Deposit(msg.sender, msg.value);
}
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
payable(msg.sender).transfer(amount);
emit Withdrawal(msg.sender, amount);
}
}
Pro Tip: Apps like Etherscan use these logs to show transaction details!
⚠️ Part 3: Error Handling in Solidity
The Three Musketeers: require, assert, revert
Solidity gives you three ways to say “Something went wrong!”
graph TD A["Error Handling"] --> B["require"] A --> C["assert"] A --> D["revert"] B --> E["Check user input<br/>Refunds remaining gas"] C --> F["Check internal bugs<br/>Uses all gas"] D --> G["Complex conditions<br/>Refunds remaining gas"]
1. require() - The Bouncer
Like a bouncer at a club checking IDs. Use it to validate:
- User inputs
- External conditions
- State requirements
function transfer(address to, uint256 amount) public {
// The bouncer checks...
require(amount > 0, "Amount must be positive");
require(to != address(0), "Invalid address");
require(balances[msg.sender] >= amount, "Not enough funds");
// If all checks pass, let them in!
balances[msg.sender] -= amount;
balances[to] += amount;
}
2. assert() - The Safety Net
Use for things that should NEVER happen if your code is correct. Like checking 1 + 1 still equals 2.
function divide(uint256 a, uint256 b) public pure
returns (uint256)
{
require(b != 0, "Cannot divide by zero");
uint256 result = a / b;
// This should mathematically always be true
assert(result * b <= a);
return result;
}
3. revert() - The Emergency Exit
Perfect for complex conditions where if-else is cleaner:
function complexCheck(uint256 value) public {
if (value < 10) {
revert("Value too small");
}
if (value > 100) {
revert("Value too large");
}
if (value == 42) {
revert("42 is not allowed!");
}
// Continue with logic...
}
Quick Comparison
| Feature | require |
assert |
revert |
|---|---|---|---|
| Use For | Input validation | Internal errors | Complex logic |
| Gas Refund | ✅ Yes | ❌ No | ✅ Yes |
| Error Message | ✅ Yes | ❌ No | ✅ Yes |
| When to Use | Most common | Rare cases | If-else needed |
🎨 Part 4: Custom Errors
The Modern Way (Solidity 0.8.4+)
Custom errors are like having specific error codes instead of just “ERROR!”
Why custom errors?
- 🔥 Use 60% less gas than string messages
- 📖 More descriptive
- 🧹 Cleaner code
// Define your custom errors at the top
error InsufficientBalance(
uint256 available,
uint256 required
);
error Unauthorized(address caller);
error InvalidAmount();
contract ModernBank {
mapping(address => uint256) public balances;
address public owner;
function withdraw(uint256 amount) public {
if (amount == 0) {
revert InvalidAmount();
}
if (balances[msg.sender] < amount) {
revert InsufficientBalance(
balances[msg.sender],
amount
);
}
// Process withdrawal...
}
function adminAction() public {
if (msg.sender != owner) {
revert Unauthorized(msg.sender);
}
// Admin logic...
}
}
Gas Savings Comparison
// OLD WAY - costs more gas 💸
require(balance >= amount, "Insufficient balance");
// NEW WAY - saves gas! 🎉
if (balance < amount) {
revert InsufficientBalance(balance, amount);
}
🎣 Part 5: Try-Catch in Solidity
When External Calls Go Wrong
Try-catch lets you handle failures when calling other contracts or creating contracts.
Think of it like ordering food delivery:
- try = “Let me try to call this other restaurant”
- catch = “If they don’t answer, here’s my backup plan”
interface ExternalContract {
function riskyFunction(uint256 value)
external
returns (uint256);
}
contract SafeCaller {
event CallSucceeded(uint256 result);
event CallFailed(string reason);
event CallFailedLowLevel(bytes reason);
function safeCall(address target, uint256 value)
public
{
ExternalContract ext = ExternalContract(target);
try ext.riskyFunction(value) returns (uint256 result) {
// Success! 🎉
emit CallSucceeded(result);
} catch Error(string memory reason) {
// Caught require/revert with message
emit CallFailed(reason);
} catch (bytes memory lowLevelData) {
// Caught other errors (assert, etc.)
emit CallFailedLowLevel(lowLevelData);
}
}
}
Try-Catch for Contract Creation
contract Token {
constructor(string memory name) {
require(bytes(name).length > 0, "Name required");
}
}
contract TokenFactory {
event TokenCreated(address tokenAddress);
event CreationFailed(string reason);
function createToken(string memory name) public {
try new Token(name) returns (Token token) {
emit TokenCreated(address(token));
} catch Error(string memory reason) {
emit CreationFailed(reason);
}
}
}
graph TD A["Call External Contract"] --> B{try} B -->|Success| C["Execute Success Code"] B -->|Fail| D{catch type?} D -->|Error string| E["catch Error#40;string#41;"] D -->|Panic/Assert| F["catch Panic#40;uint256#41;"] D -->|Unknown| G["catch#40;bytes#41;"] E --> H["Handle Gracefully"] F --> H G --> H
Important Limitations
⚠️ Try-catch only works for:
- External function calls
- Contract creation with
new
❌ Does NOT work for:
- Internal function calls
- Regular math operations
🚀 Putting It All Together
Here’s a complete example using everything we learned:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
// Custom Errors (gas efficient!)
error InsufficientFunds(uint256 available, uint256 needed);
error InvalidRecipient();
error TransferFailed();
contract CompleteExample {
// Events for logging
event Deposited(
address indexed user,
uint256 amount
);
event Transferred(
address indexed from,
address indexed to,
uint256 amount
);
event ErrorOccurred(string reason);
mapping(address => uint256) public balances;
function deposit() public payable {
require(msg.value > 0, "Must send ETH");
balances[msg.sender] += msg.value;
emit Deposited(msg.sender, msg.value);
}
function transfer(address to, uint256 amount) public {
// Custom error for better gas
if (to == address(0)) {
revert InvalidRecipient();
}
if (balances[msg.sender] < amount) {
revert InsufficientFunds(
balances[msg.sender],
amount
);
}
balances[msg.sender] -= amount;
balances[to] += amount;
emit Transferred(msg.sender, to, amount);
}
}
🎯 Key Takeaways
| Concept | Remember This |
|---|---|
| Events | Megaphone for your contract - broadcast to the world |
| indexed | Makes parameters searchable (max 3) |
| require | Bouncer - check user input |
| assert | Safety net - should never fail |
| revert | Emergency exit for complex logic |
| Custom Errors | Save 60% gas vs string messages |
| Try-Catch | Handle external call failures gracefully |
🌈 You Did It!
You now understand how smart contracts:
- 📢 Communicate with the outside world (Events)
- 📝 Keep records of what happened (Logging)
- 🛡️ Protect themselves from mistakes (Error Handling)
- 🎨 Use modern patterns (Custom Errors)
- 🎣 Handle failures gracefully (Try-Catch)
Next step: Practice by building a simple voting contract with events and proper error handling!
