akash11

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:

Solidity

Understanding 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:
  1. A transaction arrives at your contract with some Ether
  2. Solidity checks: is msg.data empty?
  3. If yes (empty data), it looks for a receive() function
  4. If receive() exists, it executes that
  5. If receive() doesn't exist, it falls back to fallback()
  6. 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.

Copyright © 2025

Akash Vaghela