Data Locations in Solidity: storage, memory, and calldata
Master the three data locations in Solidity to write gas-efficient contracts. Learn exactly when and where to use storage, memory, and calldata with practical examples.
Published on 03 November, 2025
Categories:
SolidityUnderstanding Data Locations
Every variable in Solidity lives in one of three places. Storage is like a permanent database on the blockchain that persists between function calls. Memory is temporary scratch space that exists only while your function runs. Calldata is a read-only area where external function arguments arrive.
Picking the wrong location can make your contract expensive to use or cause it to fail compilation. The location you choose affects gas costs, whether you can modify the data, and how long it exists.
State Variables Always Use storage
State variables are declared outside functions and automatically live in storage. You never specify the location keyword for them because they must persist on the blockchain.
contract StateVariables { // All these are automatically in storage uint256 public counter; address public owner; string public name; uint256[] public numbers; mapping(address => uint256) public balances; struct User { string name; uint256 age; } User public user; }
You cannot and should not try to add a location keyword to state variables. This code would not compile:
// WRONG - This will not compile uint256 memory counter; // Error: state variables cannot be memory
Local Variables in Functions
When you declare variables inside functions, the rules change based on the data type.
Value Types Need No Location
Value types like uint256, bool, address, and bytes32 are always copied. You never specify a location for them in local variables.
contract ValueTypes { uint256 public stateNumber = 100; function workWithValues() public view { uint256 localNumber = stateNumber; // No location keyword needed bool flag = true; // No location keyword address addr = msg.sender; // No location keyword localNumber = 200; // This doesn't affect stateNumber } }
Reference Types Require Location
Arrays, structs, strings, and mappings are reference types. When you use them as local variables, you must specify memory or storage.
contract ReferenceTypes { uint256[] public numbers; string public text; struct Person { string name; uint256 age; } Person public person; function workWithReferences() public { // storage creates a reference to the state variable uint256[] storage numRef = numbers; numRef.push(10); // This modifies the state variable 'numbers' // memory creates a temporary copy uint256[] memory numCopy = numbers; numCopy[0] = 999; // This only changes the copy, not the state // Same with structs Person storage personRef = person; personRef.age = 30; // Modifies state Person memory personCopy = person; personCopy.age = 50; // Only changes the copy // Strings work the same way string storage textRef = text; string memory textCopy = text; } }
Creating New Arrays and Structs
When you create new arrays or structs inside a function, they go in memory:
contract CreatingData { function createNewData() public pure { // Creating a new array uint256[] memory numbers = new uint256[](5); numbers[0] = 100; numbers[1] = 200; // Creating a new struct Person memory newPerson = Person({ name: "Alice", age: 25 }); // Creating a string string memory message = "Hello"; } struct Person { string name; uint256 age; } }
Function Parameters
The location you use for function parameters depends on whether the function is external or public, and whether you need to modify the data.
External Functions Use calldata
External functions can only be called from outside the contract. Use calldata for reference type parameters to save gas. Calldata is read-only.
contract ExternalFunctions { // calldata is most efficient for external functions function processArray(uint256[] calldata data) external pure returns (uint256) { uint256 sum = 0; for(uint256 i = 0; i < data.length; i++) { sum += data[i]; } return sum; } function processString(string calldata text) external pure returns (uint256) { return bytes(text).length; } struct Item { uint256 id; string name; } function processStruct(Item calldata item) external pure returns (uint256) { return item.id; } }
Public Functions Can Use memory or calldata
Public functions can be called externally or internally. You can use either memory or calldata, but calldata is more efficient when called externally.
contract PublicFunctions { // Using memory (works but costs more gas when called externally) function withMemory(uint256[] memory data) public pure returns (uint256) { return data.length; } // Using calldata (more efficient) function withCalldata(uint256[] calldata data) public pure returns (uint256) { return data.length; } }
Internal and Private Functions
Internal and private functions are only called from within the contract or derived contracts. You can use memory or storage for parameters.
contract InternalFunctions { uint256[] public numbers; // Internal function with storage parameter function modifyArray(uint256[] storage arr) internal { arr.push(100); // Modifies the original array } // Internal function with memory parameter function sumArray(uint256[] memory arr) internal pure returns (uint256) { uint256 total = 0; for(uint256 i = 0; i < arr.length; i++) { total += arr[i]; } return total; } function useInternalFunctions() public { modifyArray(numbers); // Pass storage reference uint256[] memory temp = new uint256[](3); temp[0] = 1; temp[1] = 2; temp[2] = 3; uint256 result = sumArray(temp); // Pass memory array } }
Value Type Parameters Never Need Location
For value types in parameters, never specify a location:
contract ParameterValueTypes { // Correct - no location for value types function doSomething(uint256 amount, address user, bool flag) public { // function body } // WRONG - this won't compile // function wrongWay(uint256 memory amount) public { } }
Return Values
Return values follow similar rules. Value types need no location, but reference types need memory or calldata.
Returning Value Types
contract ReturnValueTypes { uint256 public number = 42; // No location needed for value types function getNumber() public view returns (uint256) { return number; } function getAddress() public pure returns (address) { return address(0x123); } function getBool() public pure returns (bool) { return true; } }
Returning Reference Types
You can only return memory or calldata, never storage:
contract ReturnReferenceTypes { uint256[] public numbers; string public text; // Return memory copy function getNumbers() public view returns (uint256[] memory) { return numbers; } function getText() public view returns (string memory) { return text; } // Creating and returning new data function createArray() public pure returns (uint256[] memory) { uint256[] memory newArray = new uint256[](3); newArray[0] = 1; newArray[1] = 2; newArray[2] = 3; return newArray; } struct Person { string name; uint256 age; } function createPerson() public pure returns (Person memory) { return Person("Bob", 30); } }
Returning calldata
You can return calldata parameters directly without copying them to memory, which saves gas:
contract ReturnCalldata { function passThrough(string calldata text) external pure returns (string calldata) { return text; // No copy needed, very efficient } function filterArray(uint256[] calldata data) external pure returns (uint256[] calldata) { // You can return the same calldata without modification return data; } }
Practical Examples Combining Everything
Here is a complete contract showing all the patterns together:
contract DataLocationComplete { // State variables (always storage, no keyword) uint256 public totalSupply; address public owner; uint256[] public allIds; mapping(address => uint256) public balances; struct Token { uint256 id; string name; address owner; } Token[] public tokens; constructor() { owner = msg.sender; // Value type, no location totalSupply = 1000000; } // External function with calldata (most efficient) function addMultipleTokens(Token[] calldata newTokens) external { require(msg.sender == owner); // Reading from calldata (efficient) for(uint256 i = 0; i < newTokens.length; i++) { tokens.push(newTokens[i]); } } // Public function showing memory usage function getTokenInfo(uint256 index) public view returns (string memory, address) { // Create memory copy of struct Token memory token = tokens[index]; return (token.name, token.owner); } // Internal function using storage reference function updateTokenStorage(uint256 index, string memory newName) internal { // storage reference to modify state directly Token storage token = tokens[index]; token.name = newName; } // Internal function using memory function calculateValue(uint256[] memory prices) internal pure returns (uint256) { uint256 total = 0; for(uint256 i = 0; i < prices.length; i++) { total += prices[i]; } return total; } // Public function combining different locations function complexOperation(uint256[] calldata inputData) public returns (uint256) { // Work with storage uint256[] storage storedIds = allIds; storedIds.push(inputData[0]); // Work with memory uint256[] memory tempData = new uint256[](inputData.length); for(uint256 i = 0; i < inputData.length; i++) { tempData[i] = inputData[i] * 2; } // Call internal function with memory uint256 result = calculateValue(tempData); // Value types need no location uint256 finalResult = result + totalSupply; return finalResult; } // Function showing wrong memory usage function inefficientUpdate(uint256 index) public { // WRONG: This creates a copy, doesn't modify state Token memory token = tokens[index]; token.name = "Updated"; // Only changes the copy // CORRECT: Use storage reference Token storage tokenRef = tokens[index]; tokenRef.name = "Updated"; // Actually modifies state } }
Quick Reference Guide
State variables: No location keyword, automatically storage.
Local value types (uint, bool, address): No location keyword, always copied.
Local reference types: Must use memory or storage. Use storage to modify state, memory for temporary copies.
External function parameters: Use calldata for reference types to save gas. Value types need no keyword.
Public function parameters: Can use calldata or memory for reference types. Calldata is more efficient when called externally.
Internal/private function parameters: Use memory or storage for reference types depending on whether you want to modify state.
Return values: Use memory or calldata for reference types. Cannot return storage. Value types need no keyword.
Choosing the right data location makes your contracts cheaper to use and prevents bugs from accidentally modifying copies instead of state variables. Use storage when you need to modify state directly, memory for temporary data that lives only during function execution, and calldata for read-only external inputs to save gas.