Fallback Functions in Solidity
Special functions that catch unexpected contract interactions. The fallback handles unknown function calls, while receive handles plain Ether transfers with no data attached.
Published on 05 November, 2025
Categories:
SolidityUnderstanding the Flow
When someone sends a transaction to your contract, Solidity follows a decision tree to figure out which function to execute. Here's exactly how it works:
send Ether
|
msg.data is empty?
/ \
yes no
| |
receive() exists? fallback()
/ \
yes no
| |
receive() fallback()
Let me break this down step by step:
- A transaction arrives at your contract with some Ether
- Solidity checks: is
msg.dataempty? - If yes (empty data), it looks for a
receive()function - If
receive()exists, it executes that - If
receive()doesn't exist, it falls back tofallback() - If no (data is present), it goes straight to
fallback()
The Two Functions
receive() function:
This only handles plain Ether transfers. When someone sends Ether without calling any function, this is what runs.
contract ReceiveExample { event Received(address sender, uint amount); receive() external payable { emit Received(msg.sender, msg.value); } }
fallback() function:
This is the catch-all. It handles everything else - when someone calls a function that doesn't exist, or when Ether comes with data but no
receive() function exists.contract FallbackExample { event FallbackCalled(address sender, uint amount, bytes data); fallback() external payable { emit FallbackCalled(msg.sender, msg.value, msg.data); } }
Complete Example
Here's a contract that demonstrates the entire flow:
contract TestContract { event Log(string func, address sender, uint value, bytes data); fallback() external payable { emit Log("fallback", msg.sender, msg.value, msg.data); } receive() external payable { emit Log("receive", msg.sender, msg.value, ""); } }
Now let's test it:
contract Sender { function transferToFallback(address payable target) public payable { // Sends Ether with data -> triggers fallback() target.call{value: msg.value}(abi.encodeWithSignature("nonExist()")); } function transferToReceive(address payable target) public payable { // Sends Ether without data -> triggers receive() target.call{value: msg.value}(""); } }
When you call
transferToReceive(), the flow goes:- Ether sent with empty msg.data
- receive() exists
- receive() executes
When you call
transferToFallback(), the flow goes:- Ether sent with msg.data (function signature)
- msg.data is not empty
- fallback() executes
What Happens Without receive()?
If your contract only has fallback(), everything routes through it:
contract OnlyFallback { event Log(string message); fallback() external payable { emit Log("All Ether comes through here"); } }
The flow now looks like this:
send Ether
|
msg.data is empty?
/ \
yes no
| |
receive() exists? fallback()
|
no
|
fallback()
Everything ends up at fallback() because there's no receive() to handle plain transfers.
What Happens Without fallback()?
If you only have receive(), calls with data will fail:
contract OnlyReceive { event Log(string message); receive() external payable { emit Log("Only plain Ether accepted"); } }
When someone tries to call a non-existent function, the transaction reverts because there's no fallback() to catch it.
Real World Example
Here's how you'd use both in practice:
contract Wallet { event Deposit(address from, uint amount); event FallbackTriggered(address from, bytes data); receive() external payable { // Clean Ether deposits emit Deposit(msg.sender, msg.value); } fallback() external payable { // Someone tried to call a function that doesn't exist // or sent Ether with data emit FallbackTriggered(msg.sender, msg.data); // You might want to reject this revert("Function does not exist"); } }
This wallet accepts plain Ether transfers through receive() but rejects any calls to non-existent functions through fallback().
Testing the Flow
Here's a simple test contract to see it in action:
contract FlowTest { uint public receiveCount; uint public fallbackCount; receive() external payable { receiveCount++; } fallback() external payable { fallbackCount++; } function getBalance() public view returns (uint) { return address(this).balance; } } contract Tester { function testReceive(address payable target) public payable { // This increments receiveCount (bool success,) = target.call{value: msg.value}(""); require(success); } function testFallback(address payable target) public payable { // This increments fallbackCount (bool success,) = target.call{value: msg.value}( abi.encodeWithSignature("random()") ); require(success); } }
Deploy FlowTest, then use Tester to send transactions. Check receiveCount and fallbackCount to see which function was triggered.
Key Rules
Both functions must be external:
// Correct receive() external payable { } fallback() external payable { } // Wrong receive() public payable { } // Won't compile
To accept Ether, use payable:
// Accepts Ether receive() external payable { } // Rejects Ether receive() external { }
Neither function takes parameters or returns values:
// Correct fallback() external payable { } // Wrong fallback(uint x) external payable { } // Won't compile fallback() external payable returns (uint) { } // Won't compile
The decision tree is simple: empty data with Ether goes to receive() if it exists, otherwise to fallback(). Any data present goes straight to fallback(). This separation keeps your contract's Ether-receiving logic clean and separate from handling unexpected function calls.