Data Locations in Solidity: storage, memory, and calldata
Every variable in Solidity lives somewhere. That somewhere is called a data location, and Solidity gives you three: storage, memory, and calldata. Getting this right matters. The wrong location can silently break your contract by updating a copy instead of the real state, or waste gas on unnecessary data copying.
This post covers how each location works, when to use each one, and the specific rules that apply to state variables, function parameters, local variables, and return values.
The Three Locations
storage is persistent. It lives on the blockchain and survives between function calls. Every state variable in your contract lives in storage.
memory is temporary. It exists only while a function is executing. Once the function returns, memory is gone.
calldata is read-only. When someone calls an external function, the arguments arrive in calldata. You cannot modify calldata. You can only read from it.
The location you choose affects three things: gas cost, whether you can modify the data, and how long the data exists.
State Variables
State variables are declared at the contract level, outside of any function. They always live in storage. You never add a location keyword to a state variable.
contract TokenRegistry {
uint256 public totalSupply;
address public owner;
string public name;
uint256[] public allIds;
mapping(address => uint256) public balances;
struct Token {
uint256 id;
string name;
address owner;
}
Token[] public tokens;
}
Adding a location keyword to a state variable is a compile error:
// This will not compile
uint256 memory counter;
State variables are storage by definition. The keyword is not allowed and not needed.
Value Types vs Reference Types
Before looking at function variables and parameters, you need to understand one distinction: value types vs reference types.
Value types include uint, int, bool, address, bytes1 through bytes32, and enums. When you assign a value type, Solidity copies it. There is no "reference" to the original. Because of this, value types never need a location keyword.
Reference types include arrays, structs, strings, and bytes. When you assign a reference type, it can either point to the original or be a new copy, depending on the location you specify. This is where the choice matters.
Local Variables Inside Functions
When you declare a local variable of a reference type inside a function, you must choose storage or memory.
storage gives you a reference
Using storage creates a pointer to a state variable. Changes through that pointer modify the actual state.
contract Example {
uint256[] public numbers;
function addToState() public {
// storageRef points to the state variable 'numbers'
uint256[] storage storageRef = numbers;
storageRef.push(42); // This modifies 'numbers' in state
}
}memory gives you a copy
Using memory creates a temporary copy of the data. Changes to it do not touch the state variable.
contract Example {
uint256[] public numbers;
function readOnly() public view {
// memoryCopy is a separate copy, changes here go nowhere
uint256[] memory memoryCopy = numbers;
memoryCopy[0] = 999; // Only modifies the local copy
}
}
The same rule applies to structs:
contract Example {
struct Person {
string name;
uint256 age;
}
Person public person;
function updateAge() public {
// Wrong: creates a copy, state is unchanged
Person memory copy = person;
copy.age = 30;
// Correct: storage reference, state is updated
Person storage ref = person;
ref.age = 30;
}
}
Using
memorywhen you meantstorageis a silent bug. The compiler will not warn you. The function will run, the copy will be modified, and the state variable will remain unchanged. Always double-check which one you need before writing a local reference type variable.
Creating new data in memory
When you create a new array or struct inside a function, it always goes in memory:
function buildArray() public pure returns (uint256[] memory) {
uint256[] memory result = new uint256[](3);
result[0] = 10;
result[1] = 20;
result[2] = 30;
return result;
}
Value types in local variables need no location keyword:
function calculate() public view returns (uint256) {
uint256 total = 0; // No keyword needed
bool found = false; // No keyword needed
address sender = msg.sender; // No keyword needed
return total;
}
Function Parameters
The right location for a function parameter depends on the function visibility and whether you need to modify the data.
External functions: use calldata
External functions can only be called from outside the contract. For reference type parameters, calldata is the most efficient choice. The data arrives in calldata already, so using calldata means no copying.
contract TokenRegistry {
struct Token {
uint256 id;
string name;
address owner;
}
function registerTokens(Token[] calldata newTokens) external {
for (uint256 i = 0; i < newTokens.length; i++) {
// Reading from calldata is cheap
// Cannot modify newTokens[i] directly
}
}
function getNameLength(string calldata name) external pure returns (uint256) {
return bytes(name).length;
}
}
Because calldata is read-only, you cannot modify the parameter. If you need to modify the data inside the function, copy it to memory first.
Public functions: calldata or memory
Public functions can be called externally or from within the contract. Both calldata and memory are valid for reference type parameters.
calldata costs less gas when called externally. memory is required when the function is called internally, since internal calls cannot pass calldata directly.
contract Example {
// Works, but costs more gas when called externally
function withMemory(uint256[] memory data) public pure returns (uint256) {
return data.length;
}
// More efficient for external callers
function withCalldata(uint256[] calldata data) public pure returns (uint256) {
return data.length;
}
}
For public functions that are called mostly externally, prefer calldata. For functions that are also called internally, memory is the safer default.
Internal and private functions: memory or storage
Internal and private functions cannot be called from outside the contract. You can pass either a memory copy or a storage reference.
contract Example {
uint256[] public numbers;
// Internal function that modifies state via storage reference
function addToArray(uint256[] storage arr, uint256 value) internal {
arr.push(value);
}
// Internal function that reads from a memory array
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 run() public {
addToArray(numbers, 100); // Passes storage reference
uint256[] memory temp = new uint256[](3);
temp[0] = 1;
temp[1] = 2;
temp[2] = 3;
uint256 result = sumArray(temp); // Passes memory array
}
}
Value type parameters never take a location keyword, regardless of function visibility:
// Correct
function transfer(uint256 amount, address to, bool verified) public { }
// Compile error
// function transfer(uint256 memory amount) public { }
Return Values
Value types returned from functions need no location keyword. Reference types must use memory or calldata. You cannot return a storage reference.
Returning reference types from state
When a function returns an array or struct from state, Solidity copies it into memory first:
contract Example {
uint256[] public numbers;
string public text;
function getNumbers() public view returns (uint256[] memory) {
return numbers; // Copied from storage to memory
}
function getText() public view returns (string memory) {
return text;
}
}Returning newly created data
When you create data inside a function and return it, it must be memory:
function buildRange(uint256 n) public pure returns (uint256[] memory) {
uint256[] memory result = new uint256[](n);
for (uint256 i = 0; i < n; i++) {
result[i] = i;
}
return result;
}Returning calldata directly
If a function receives a calldata parameter and you want to return it unchanged, you can return calldata directly. This avoids copying and saves gas:
function passThrough(string calldata input) external pure returns (string calldata) {
return input; // No copy, very efficient
}
This only works when you return the same calldata without modification.
A Full Example
Here is a contract that uses all three locations together, showing how they interact:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract TokenRegistry {
// State variables: always storage, no keyword
address public owner;
uint256 public totalCount;
struct Token {
uint256 id;
string name;
address tokenOwner;
}
Token[] public tokens;
mapping(address => uint256[]) public ownerTokenIds;
constructor() {
owner = msg.sender; // value type, no keyword
totalCount = 0;
}
// External: calldata for reference types
function batchRegister(Token[] calldata newTokens) external {
require(msg.sender == owner, "Not owner");
for (uint256 i = 0; i < newTokens.length; i++) {
tokens.push(newTokens[i]);
ownerTokenIds[newTokens[i].tokenOwner].push(newTokens[i].id);
totalCount++;
}
}
// Public: returns memory copy from state
function getToken(uint256 index) public view returns (Token memory) {
return tokens[index];
}
// Internal: storage reference to modify state directly
function updateName(uint256 index, string memory newName) internal {
Token storage token = tokens[index]; // storage reference
token.name = newName; // modifies state
}
// Internal: pure calculation using memory
function sumIds(uint256[] memory ids) internal pure returns (uint256) {
uint256 total = 0;
for (uint256 i = 0; i < ids.length; i++) {
total += ids[i];
}
return total;
}
// Public: mixes calldata input with storage and memory
function processAndUpdate(
uint256[] calldata inputIds,
uint256 updateIndex,
string memory newName
) public returns (uint256) {
// Work with storage
uint256[] storage ownerIds = ownerTokenIds[msg.sender];
ownerIds.push(inputIds[0]); // modifies state
// Work with memory
uint256[] memory tempIds = new uint256[](inputIds.length);
for (uint256 i = 0; i < inputIds.length; i++) {
tempIds[i] = inputIds[i];
}
updateName(updateIndex, newName); // passes memory string to internal
return sumIds(tempIds); // passes memory array
}
}
Quick Reference
| Context | Value types | Reference types |
|---|---|---|
| State variables | No keyword (always storage) | No keyword (always storage) |
| Local variable pointing to state | No keyword | storage |
| Local variable for temporary use | No keyword | memory |
| External function parameter | No keyword | calldata (preferred) |
| Public function parameter | No keyword | calldata or memory |
| Internal/private function parameter | No keyword | memory or storage |
| Return value | No keyword | memory or calldata |
The pattern is consistent once you see it. Value types are always copied, so location never applies to them. Reference types need a location whenever they appear as local variables, parameters, or return values, because Solidity needs to know whether to create a reference or a copy.
Use storage when you need to modify state. Use calldata for external inputs you will not modify. Use memory for everything else.