Fallback Functions in Solidity

Solidity has two special functions that handle incoming transactions your contract did not explicitly plan for: receive() and fallback(). They look similar, but they serve different purposes. This post explains what each function does, how Solidity decides which one to call, and how to use them correctly.

How Solidity Routes Incoming Transactions

When a transaction arrives at your contract, Solidity runs through a decision tree before executing any code.

First, it checks whether msg.data is empty.

If msg.data is empty, Solidity looks for a receive() function. If one exists, it runs. If not, Solidity falls back to fallback().

If msg.data is not empty, Solidity skips receive() entirely and goes straight to fallback().

Here is that logic as a diagram:

         incoming transaction
                 |
        msg.data is empty?
           /           \
         yes             no
          |               |
  receive() exists?    fallback()
      /        \
    yes          no
     |            |
 receive()     fallback()

The separation is intentional. Plain Ether transfers have no data. Function calls and other interactions do. This lets you handle each case with different logic.

The Two Functions

receive()

receive() handles plain Ether transfers. When someone sends Ether to your contract 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);
    }
}

receive() must be external and payable. It takes no parameters and returns nothing.

fallback()

fallback() is the catch-all. It runs when someone calls a function that does not exist on your contract, or when Ether arrives with data but no receive() function is defined.

contract FallbackExample {
    event FallbackCalled(address sender, uint amount, bytes data);

    fallback() external payable {
        emit FallbackCalled(msg.sender, msg.value, msg.data);
    }
}

Like receive(), fallback() must be external. The payable modifier is optional. Without it, your fallback function will reject any Ether sent with the call.

A Complete Example

This contract implements both functions and logs which one was triggered:

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, "");
    }
}

And here is a contract to test it:

contract Sender {
    function sendToReceive(address payable target) public payable {
        // Empty data, so receive() runs
        target.call{value: msg.value}("");
    }

    function sendToFallback(address payable target) public payable {
        // Non-empty data, so fallback() runs
        target.call{value: msg.value}(abi.encodeWithSignature("nonExistentFunc()"));
    }
}

When sendToReceive runs, msg.data is empty, so receive() executes. When sendToFallback runs, msg.data contains the encoded function signature, so fallback() executes.

What Happens With Only One Function

Only fallback(), no receive()

If you define fallback() but not receive(), all incoming transactions route through fallback(). Plain Ether transfers and calls with data both end up there.

contract OnlyFallback {
    event Log(string message);

    fallback() external payable {
        emit Log("all transactions come here");
    }
}

This works, but you lose the ability to distinguish between plain transfers and unexpected function calls.

Only receive(), no fallback()

If you define receive() but not fallback(), calls to non-existent functions will revert. There is nothing to catch them.

contract OnlyReceive {
    event Log(string message);

    receive() external payable {
        emit Log("plain ether accepted");
    }
}

Plain Ether transfers work fine. Any call with data causes a revert.

A Real Wallet Example

Here is a pattern you will see in practice. The contract accepts plain Ether but explicitly rejects calls to functions that do not exist:

contract Wallet {
    event Deposit(address from, uint amount);
    event FallbackTriggered(address from, bytes data);

    receive() external payable {
        emit Deposit(msg.sender, msg.value);
    }

    fallback() external payable {
        emit FallbackTriggered(msg.sender, msg.data);
        revert("Function does not exist");
    }
}

Using revert in fallback() is a common defensive pattern. If someone accidentally sends a transaction with the wrong function signature, the call fails cleanly instead of silently doing nothing.

Testing Which Function Runs

This contract lets you verify the routing yourself:

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 {
        (bool success,) = target.call{value: msg.value}("");
        require(success);
    }

    function testFallback(address payable target) public payable {
        (bool success,) = target.call{value: msg.value}(
            abi.encodeWithSignature("random()")
        );
        require(success);
    }
}

Deploy FlowTest, then call testReceive and testFallback from Tester. Check receiveCount and fallbackCount to see exactly which function ran.

Key Rules

Both receive() and fallback() must be external. Using public will not compile:

// Correct
receive() external payable { }
fallback() external payable { }

// Will not compile
receive() public payable { }

To accept Ether, the function must be payable. Without it, any Ether sent with the transaction causes a revert:

// Accepts Ether
receive() external payable { }

// Rejects Ether
receive() external { }

Neither function takes parameters or returns values. Attempting this will not compile:

// Will not compile
fallback(uint x) external payable { }
fallback() external payable returns (uint) { }

If neither receive() nor fallback() is defined, your contract cannot accept Ether at all. Any transfer will revert. This is a common source of bugs when integrating with contracts that send Ether back to callers.

Summary

receive() handles plain Ether transfers with empty msg.data. fallback() handles everything else: unknown function calls and Ether transfers with data when no receive() exists. Having both gives you clean separation between the two cases. Having only one or the other has specific tradeoffs worth understanding before you deploy.