Mappings in Solidity: Every Key Already Exists
The most common state shape in Solidity, and the one people misread first. What a mapping is, why there is no not-found, how nested mappings read, and why you cannot count the keys.
TL;DR
- A mapping is a lookup from a key to a value. You hand it a key in square brackets and it hands back a value. That is the whole idea.
- Every possible key already exists, and an untouched one answers with the zero value for its type. A mapping never says "not found".
- A nested mapping reads left to right: in
allowance[owner][spender], the first key chooses a table and the second chooses an entry in it. - You cannot loop over a mapping or count its keys, because no list of keys is stored anywhere.
publicon a mapping generates a getter that takes the key as an argument, so a mapping is read one entry at a time.
What is a mapping in Solidity?
A mapping is a key-to-value lookup table held in chain storage. You write the key in square brackets to read or write the value at it. Every possible key already exists and starts at the zero value for its type, which means a mapping can never report that an entry is missing.
Here is the top of Uniswap V2's token contract. Three of the six lines are labels; two are lookup tables:
string public constant name = 'Uniswap V2';
string public constant symbol = 'UNI-V2';
uint8 public constant decimals = 18;
uint public totalSupply;
mapping(address => uint) public balanceOf;
mapping(address => mapping(address => uint)) public allowance;
Read the declaration function before name. mapping(address => uint) says: give me an address, I give you a number. Only then does balanceOf say what that number means. The type is the mechanism, the name is the intent. That gives you a three-move recipe for any mapping you meet:
- What is the key type? Here,
address, so entries are per account. - What is the value type? Here,
uint, so each entry is a single unsigned number. - What does one entry mean? Here, how many LP tokens that account holds.
Do them before you read a single function, as part of the state pass in state variables in Solidity.
Why is there no not found in a mapping?
Because nothing is ever inserted. A mapping is not a collection you add to, it is a lookup that already answers for every key in its key type. An address nobody has ever mentioned answers 0 exactly as confidently as an address that spent it all.
There is no null, no KeyError, no undefined. balanceOf[someRandomAddress] is not an error, it is 0, so a zero and an absence look identical. A balance of zero could be an account that never existed or one that withdrew everything, and the contract cannot tell them apart either, because storage holds the same thing in both cases.
Contracts needing the distinction build it. ENS keeps its registry in a mapping and synthesises the missing concept by hand:
function recordExists(
bytes32 node
) public view virtual override returns (bool) {
return records[node].owner != address(0x0);
}
That is a contract inventing "exists" out of a value comparison, by declaring the zero address to mean "not really here". Every design that needs existence does something in this family.
How do you read a nested mapping?
Read it left to right. In mapping(address => mapping(address => uint)), the outer key selects an inner table and the inner key selects an entry in that table. When you see two sets of square brackets, the first bracket is the owner of the sub-table and the second is the entry within it.
The allowance line from Uniswap V2 is the canonical example, and the clearest way in is watching it written:
function _approve(address owner, address spender, uint value) private {
allowance[owner][spender] = value;
emit Approval(owner, spender, value);
}
allowance[owner] is everything one owner has permitted; [spender] narrows it to what one spender may take. Every owner has their own table, and the tables never collide because the keys are part of where the value lives. Reading it back in transferFrom has exactly the same shape, allowance[from][msg.sender].
Order matters: allowance[spender][owner] is a different entry, in a different table, holding a different number. When tracing one, say the keys out loud in order and check them against the declaration.
Can a mapping value be a struct?
Yes, and it is very common. When one key needs to carry several facts rather than a single number, the value type becomes a struct and each field is read or written independently. The key still selects exactly one entry; that entry is simply wider.
The ENS registry does this, and its whole state section is two lines:
struct Record {
address owner;
address resolver;
uint64 ttl;
}
mapping(bytes32 => Record) records;
mapping(address => mapping(address => bool)) operators;
One key, a bytes32 name hash, carries three facts. The recipe still works: key is a name hash, value is a Record, one entry is everything the registry knows about that name. Fields are read and written one at a time, which is the two-level access in recordExists above: records[node] picks the entry, .owner picks the field inside it.
Note the second line too. operators is a nested mapping whose value is a bool, the standard shape for "has A authorised B", and whose zero value, false, is exactly the right default.
What does mapping(address account => uint256) mean?
It means the same as mapping(address => uint256). Newer Solidity lets you name the key and the value inside the declaration. The names are documentation for the reader and change nothing about how the mapping behaves, what it stores, or how it is accessed.
OpenZeppelin's current ERC20 uses the style throughout:
mapping(address account => uint256) private _balances;
mapping(address account => mapping(address spender => uint256)) private _allowances;
Compare that with the Uniswap V2 lines above. The types are identical. The only difference is that the nested declaration tells you which bracket is which without working it out from the function bodies. It is the same lookup table with labels on the door, available only in recent compiler versions, which is why older contracts never have it.
Why can you not loop over a mapping?
Because no list of keys is stored. The slot holding an entry is computed from the key itself when you ask for it, so values sit at scattered locations and nothing records which keys were used. There is no first entry to start from and no count to stop at.
A mapping is an address book where every possible name already has a page, most pages are blank, and there is no index at the front. You can turn to any page instantly and never list the ones with writing on them. Where each page lives is covered in EVM storage layout.
When a contract needs the list, it keeps one itself. OpenZeppelin's EnumerableSet is the standard version, and its core is the two pieces side by side:
struct Set {
// Storage of set values
bytes32[] _values;
// Position is the index of the value in the `values` array plus 1.
// Position 0 is used to mean a value is not in the set.
mapping(bytes32 value => uint256) _positions;
}
An array for "which keys exist, in order" and a mapping for "where is this one, instantly". Notice the comment: the position is the index plus one, precisely so the zero value can mean absence. The more common answer, though, is that contracts keep no list at all. They emit an event whenever an entry changes, and anything needing the full picture rebuilds it off chain.
What does public do on a mapping?
It generates a getter that takes the key as an argument. mapping(address => uint) public balanceOf produces a function balanceOf(address) returning a uint. A nested public mapping produces a getter taking both keys. As with any state variable, public grants reading, never writing.
That is why balanceOf and allowance in Uniswap V2 satisfy the ERC20 interface without anyone writing those functions. The compiler wrote them, because the variables are public. OpenZeppelin reaches the same interface the other way, with private mappings and hand-written functions:
function allowance(address owner, address spender) public view virtual returns (uint256) {
return _allowances[owner][spender];
}
From outside the two are indistinguishable. Inside, one let the compiler generate the accessor and the other wrote it out so it could be marked virtual and overridden by a child. A hand-written getter that only returns a variable usually exists for that reason. The keyword rules behind both are in Solidity visibility.
Delete the line and ask what breaks
- Delete
publicfrommapping(address => uint) public balanceOfand the generated getter goes with it. Balances are still stored and still readable by anyone querying the chain directly, but no longer reachable through the contract's interface. - Swap the two keys in
allowance[owner][spender]and it still compiles and still runs. It just reads a different entry, a change that costs nothing to make and everything to find. - Change the value type of
balanceOffromuinttobooland every balance collapses to a yes or no. The keys are untouched; the value type is the ceiling on what an entry can say, as covered in Solidity data types. - Remove the
_valuesarray fromEnumerableSetand you are back to a plain mapping: instant to look up, impossible to enumerate.
Related questions
What is a mapping in Solidity? A key-to-value lookup table stored on chain. You access an entry by writing the key in square brackets. Every key already exists and returns the zero value until something writes to it.
Can you iterate over a mapping in Solidity? No. Mappings store no list of their keys, so there is nothing to iterate. Contracts needing enumeration keep a separate array of keys alongside the mapping, or emit events and rebuild the list off chain.
What does a mapping return for a key that was never set?
The zero value for its value type: 0 for numbers, false for booleans, the zero address for addresses, an all-zero struct for structs. That is indistinguishable from a key set and then cleared.
How do nested mappings work?
The value type of the outer mapping is itself a mapping. allowance[owner][spender] applies the outer key first to select a sub-table, then the inner key to select an entry in it. Bracket order matches declaration order.
Does public on a mapping let anyone change it? No. It generates a read-only getter taking the key as an argument. Only code inside the declaring contract can write to it, and only through functions that contract defines.
What does mapping(address account => uint256) mean?
Exactly the same as mapping(address => uint256). Newer Solidity versions allow naming the key and value in the declaration. The names are for readers only, with no effect on behaviour or storage.
Where to go next
Mappings are the most common state shape in Solidity and the one whose behaviour is least like what the syntax suggests. Two facts carry the load: every key already exists and answers with zero, and no list of keys is kept anywhere.
Next, where those entries live. The slot holding balanceOf[someAddress] is calculated rather than allocated, and that calculation is what makes storage stop feeling like magic: EVM storage layout.
Tagged