You just have to add one tiny change in your contracts. You think this will take you only a few seconds. And you are right, adding the code took you less than a minute. All happy about your coding speed you enter the compile command. With such a small change, you are confident your code is correct.

But what's that? You see the message:

InternalCompilerError: Stack too deep, try removing local variables.

Ouch. What happened here? Chances are if you've written a few contracts in your career before, this is a very familiar error message and comes up at unpredictable times. But usually when you are short on time.

Forrest Gump Box of Chocolate

Don't worry though, it's not your fault. If you are struggling with this error, you are not the only one.

Just take a look at this recent poll:

Most Hated Survey Results

Why does this error exist?

But Why Cat

This is usually a compiler code-generation problem with too many live values for convenient stack access. The EVM stack is not limited to 16 total items: legacy DUP and SWAP instructions only directly reach a limited number of entries near its top. Which Solidity source triggers the error depends on the compiler, optimizer and code-generation path, so small refactors can change the result.

But I don’t want to bore you with too much theory. This is supposed to be a practical blog post.

I don't care, just show me how to solve it

Now what exactly are some general approaches to fixing this? Let's look at five ways to deal with the error.

Modern compiler option: For a current Solidity compiler, try the IR pipeline (viaIR: true in compiler settings, usually with the optimizer enabled). It can resolve cases that fail in the legacy code generator. Review the documented semantic differences and run your tests before changing an existing build. The examples below remain pinned to 0.7.1.

1. Use less variables

Homer Simpson

2. Utilizing functions

3. Block scoping

4. Utilizing structs

5. Some Hacking

Okay the first one is obvious. If you can, try and refactor your code to simply use less variables. Not much further to it. Got it? Great, let’s  move on.

For the other four, let's look at a stack too deep example code and four different ways to fix it.

Stack Too Deep Example

Let's look at the following code. It will throw our beloved stack too deep error message. What can we do about it?

soliditySolidity 0.7.1
// SPDX-License-Identifier: MIT
pragma solidity 0.7.1;

contract StackTooDeepTest1 {
    function addUints(
        uint256 a,uint256 b,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
    ) external pure returns(uint256) {
        
        return a+b+c+d+e+f+g+h+i;
    }
}

1. Use an internal function

Yes using an internal function will make the error go away. For example we can split it into three function calls each adding up three uints. Oddly enough, the stack too deep error can force us to write better code.

soliditySolidity 0.7.1
// SPDX-License-Identifier: MIT
pragma solidity 0.7.1;

contract StackTooDeepTest1 {
   function addUints(
        uint256 a,uint256 b,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
    ) external pure returns(uint256) {
        
        return _addThreeUints(a,b,c) + _addThreeUints(d,e,f) + _addThreeUints(g,h,i);
    }
    
    function _addThreeUints(uint256 a, uint256 b, uint256 c) private pure returns(uint256) {
        return a+b+c;
    }
}

2. Make use of block scoping

Inspired by Uniswap you can also use block scoping. Simply put curly brackets around parts of the code:

soliditySolidity 0.7.1
// SPDX-License-Identifier: MIT
pragma solidity 0.7.1;

contract StackTooDeepTest2 {
    function addUints(
        uint256 a,uint256 b,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
    ) external pure returns(uint256) {
        
        uint256 result = 0;
        
        {
            result = a+b+c+d+e;
        }
        
        {
            result = result+f+g+h+i;
        }

        return result;
    }
}

3.  Pass structs instead

This is kind of a way to just use less variables. Put data in a struct. More often than not this is a good idea for readability reasons anyways.

soliditySolidity 0.7.1
// SPDX-License-Identifier: MIT
pragma solidity 0.7.1;
pragma experimental ABIEncoderV2;

contract StackTooDeepTest3 {
    struct UintPair {
        uint256 value1;
        uint256 value2;
    }
    
    function addUints(
        UintPair memory a, UintPair memory b, UintPair memory c, UintPair memory d, uint256 e
    ) external pure returns(uint256) {
        
        return a.value1+a.value2+b.value1+b.value2+c.value1+c.value2+d.value1+d.value2+e;
    }
}

4. Parsing msg.data

Original idea for this approach came from user k06a at Stackexchange. It's somewhat a hacky solution, so I generally wouldn't recommend it. But maybe you tried all the other ones without success? Then give this a try:
soliditySolidity 0.7.1
// SPDX-License-Identifier: MIT
pragma solidity 0.7.1;

contract StackTooDeepTest4 {
    function addUints(
        uint256 /*a*/,uint256 /*b*/,uint256 c,uint256 d,uint256 e,uint256 f,uint256 g,uint256 h,uint256 i
    ) external pure returns(uint256) {

        return _fromUint(msg.data)+c+d+e+f+g+h+i;
    }
    
    function _fromUint(bytes memory data) internal pure returns(uint256 value) {
        uint256 value1;
        uint256 value2;

        assembly {
            value1 := mload(add(data, 36))
            value2 := mload(add(data, 68))
            value  := add(value1, value2)
        }
    }
}

How this works is by parsing the msg.data. All data being sent to the contract is stored here, so we can comment out variable a and b, but still receive their values. The first 4 bytes of the msg.data is the function selector data. After that come our first two uint256 each with 32 bytes.

This works only for external functions given the msg.data being used. A workaround for using it also with public functions would be calling those public functions via this.myPublicFunction().

Calling this.myPublicFunction() creates a new external call and changes msg.sender to the contract, so it is not a behavior-preserving refactor. Prefer typed arguments and ordinary refactoring. Also remember that assembly arithmetic is unchecked even in Solidity 0.8.

May the stack now always be deep enough for you.