Section 01 · Definition
What is a for loop in Solidity?
A for loop repeats a block of code a controlled number of times. The three part header gives you an initializer, a condition, and a post statement in a single compact line.
Quick answer
In one sentence: A Solidity for loop runs a block of code repeatedly, checking a condition before each iteration and executing a post statement after each body, until the condition is false or the loop hits a break.
The syntax mirrors C and JavaScript almost exactly. The only meaningful difference is that every iteration costs gas, so the loop body and the iteration count both have a direct impact on your transaction cost.
// Example 1 — count from 1 to 5
for (uint256 i = 1; i <= 5; i++) {
// runs 5 times: i = 1, 2, 3, 4, 5
}
// Example 2 — sum an array of numbers
uint256[] memory scores; // e.g. [10, 20, 30]
uint256 total;
for (uint256 i = 0; i < scores.length; i++) {
total += scores[i]; // after loop: total = 60
}
// Gas-optimised form used in production contracts
uint256 len = scores.length; // cache length once
for (uint256 i = 0; i < len; ) {
total += scores[i];
unchecked { ++i; } // safe: counter can never overflow
}The gas-optimised form caches the array length in a local variable before the loop and moves the increment into an unchecked block. The unchecked keyword skips the overflow guard on the increment, saving roughly 30 gas per iteration. You will see this pattern in almost every production Solidity contract.
Section 02 · Syntax
The three parts of the for loop header
Each part of the header is optional and has a specific job. Understanding what each part does prevents common off-by-one and infinite loop bugs.
Initializer
Runs once before the first iteration. Almost always declares and sets the loop counter: uint256 i = 0. You can declare the counter here or outside the loop. If declared inside, its scope is limited to the loop body, which is cleaner.
Condition
Evaluated before every iteration, including the first. If the condition is false on entry, the body never runs. If you omit the condition entirely, the loop is infinite and requires a break to exit. The most common form is i < arr.length or i < limit.
Post statement
Runs after every iteration, before the next condition check. Almost always ++i or i++. Prefix increment (++i) costs 3 fewer gas than postfix (i++) in Solidity because postfix returns the old value, requiring an extra copy. Always use ++i in loops.
Off-by-one: the most common for loop bug
The condition i < arr.length reads indices 0 through arr.length minus 1, which is correct for arrays. Using i <= arr.length reads one index past the end and causes a panic revert. Get this direction right: less than for arrays, less than or equal to only when the bound is not an array length.
Section 03 · Control flow
break and continue inside for loops
Two keywords change how a for loop exits mid-run. Both are supported and both have real uses in smart contracts.
// break — stop as soon as we find the target score
uint256[] memory scores; // e.g. [40, 75, 90, 55]
uint256 target = 90;
bool found = false;
for (uint256 i = 0; i < scores.length; i++) {
if (scores[i] == target) {
found = true;
break; // no need to keep looping
}
}
// continue — skip failing scores, sum only passing ones (>= 50)
uint256 passingTotal;
for (uint256 i = 0; i < scores.length; i++) {
if (scores[i] < 50) {
continue; // skip this score
}
passingTotal += scores[i]; // only runs for 75, 90, 55
}Use break when searching for the first match — there is no reason to keep iterating once the answer is found. Use continue to skip records that fail a filter condition. Both are more readable than wrapping the entire body in an if block.
break in nested loops exits only the innermost loop
If you have a for loop inside another for loop, break exits only the inner one. The outer loop continues from the next iteration. If you need to exit both loops at once, use a boolean flag set before the inner loop and checked in the outer loop condition.
Section 04 · Common patterns
Four for loop patterns used in real Solidity contracts
These four shapes cover the vast majority of on-chain iteration you will encounter. Each has a different goal and a slightly different form.
Forward accumulate
Walk the array from index 0 to length minus 1, add each value to a running total. Used for sum of balances, total votes cast, total staked amount. The result is a single uint256 returned at the end.
Find maximum
Track two variables — the current winner index and its score. On each iteration compare the current element to the stored maximum and update both variables if the new element is higher. Used for finding the leading bidder, the top scorer, the most voted proposal.
Filter and collect
Build a new array of elements that pass a test. Declare a memory array with the maximum possible size, fill it element by element using a separate write index, then return a slice. Used for finding all active participants, all passing students, all whitelisted addresses.
Batch execute
Call a function or update state for every element in a list. Used for distributing rewards to a recipient list, initialising every seat in a fixed roster, or batch approving a set of addresses. The most gas expensive pattern — each element typically costs one SSTORE.
// Pattern 1: accumulate — add up all scores
uint256[] memory scores; // [10, 40, 25, 60]
uint256 total;
for (uint256 i = 0; i < scores.length; i++) {
total += scores[i]; // total = 135
}
// Pattern 2: find maximum — highest score wins
uint256 highScore;
uint256 winnerIndex;
for (uint256 i = 0; i < scores.length; i++) {
if (scores[i] > highScore) {
highScore = scores[i];
winnerIndex = i; // winnerIndex = 3 (score 60)
}
}Section 05 · Full contract
Complete example: SimpleVoting smart contract
A real-world voting contract where anyone can vote for a candidate. It uses all four for loop patterns: sum total votes, find the winner, filter leading candidates, and batch reset. Deploy on Remix and try every function.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title SimpleVoting
/// @notice On-chain voting contract — add candidates, cast votes, read results.
/// Demonstrates all four for loop patterns in one deployable contract.
/// Cap candidates at MAX_CANDIDATES to keep loops gas-safe.
contract SimpleVoting {
// ── Constants ────────────────────────────────────────────────────
uint256 public constant MAX_CANDIDATES = 20;
// ── State ────────────────────────────────────────────────────────
address public owner;
bool public votingOpen;
struct Candidate {
string name;
uint256 votes;
}
Candidate[] public candidates;
mapping(address => bool) public hasVoted;
mapping(address => uint256) public votedFor; // voter => candidate index
// ── Events ───────────────────────────────────────────────────────
event CandidateAdded(uint256 indexed index, string name);
event VoteCast(address indexed voter, uint256 indexed candidateIndex);
event VotingOpened();
event VotingClosed();
// ── Modifiers ────────────────────────────────────────────────────
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
constructor() {
owner = msg.sender;
votingOpen = false;
}
// ── Setup ────────────────────────────────────────────────────────
/// Add a candidate before voting opens. Max 20 candidates.
function addCandidate(string calldata _name) external onlyOwner {
require(!votingOpen, "voting already open");
require(candidates.length < MAX_CANDIDATES, "too many candidates");
candidates.push(Candidate({ name: _name, votes: 0 }));
emit CandidateAdded(candidates.length - 1, _name);
}
function openVoting() external onlyOwner { votingOpen = true; emit VotingOpened(); }
function closeVoting() external onlyOwner { votingOpen = false; emit VotingClosed(); }
// ── Vote ─────────────────────────────────────────────────────────
/// Cast one vote for a candidate by index.
function vote(uint256 _candidateIndex) external {
require(votingOpen, "voting is closed");
require(!hasVoted[msg.sender], "already voted");
require(_candidateIndex < candidates.length, "invalid candidate");
candidates[_candidateIndex].votes += 1;
hasVoted[msg.sender] = true;
votedFor[msg.sender] = _candidateIndex;
emit VoteCast(msg.sender, _candidateIndex);
}
// ── Read — all four for loop patterns ────────────────────────────
/// Pattern 1: accumulate — sum all votes cast so far.
function totalVotes() external view returns (uint256 total) {
uint256 len = candidates.length;
for (uint256 i = 0; i < len; ) {
total += candidates[i].votes;
unchecked { ++i; }
}
}
/// Pattern 2: find maximum — return the candidate with the most votes.
function winner()
external view
returns (string memory name, uint256 voteCount, uint256 index)
{
require(candidates.length > 0, "no candidates");
uint256 len = candidates.length;
for (uint256 i = 0; i < len; ) {
if (candidates[i].votes > voteCount) {
voteCount = candidates[i].votes;
name = candidates[i].name;
index = i;
}
unchecked { ++i; }
}
}
/// Pattern 3: filter — return only candidates that have at least one vote.
function leadingCandidates(uint256 minVotes)
external view
returns (string[] memory names, uint256[] memory votes)
{
uint256 len = candidates.length;
// First pass: count how many qualify.
uint256 count;
for (uint256 i = 0; i < len; ) {
if (candidates[i].votes >= minVotes) {
unchecked { ++count; }
}
unchecked { ++i; }
}
// Second pass: fill result arrays.
names = new string[](count);
votes = new uint256[](count);
uint256 j;
for (uint256 i = 0; i < len; ) {
if (candidates[i].votes >= minVotes) {
names[j] = candidates[i].name;
votes[j] = candidates[i].votes;
unchecked { ++j; }
}
unchecked { ++i; }
}
}
/// Pattern 4: batch execute — reset all vote counts (owner only).
function resetVotes() external onlyOwner {
require(!votingOpen, "close voting first");
uint256 len = candidates.length;
for (uint256 i = 0; i < len; ) {
candidates[i].votes = 0;
unchecked { ++i; }
}
}
/// Return all candidate names and their current vote counts.
function allResults()
external view
returns (string[] memory names, uint256[] memory votes)
{
uint256 len = candidates.length;
names = new string[](len);
votes = new uint256[](len);
for (uint256 i = 0; i < len; ) {
names[i] = candidates[i].name;
votes[i] = candidates[i].votes;
unchecked { ++i; }
}
}
function candidateCount() external view returns (uint256) {
return candidates.length;
}
}Deploy this on Remix in under two minutes: call addCandidate three times with different names, call openVoting, vote from different accounts, then call winner and totalVotes to see all four for loop patterns running live.
Section 06 · Common mistakes
Four for loop mistakes to avoid
These errors appear in real audits. Each one either causes a revert, costs unnecessary gas, or introduces a security bug.
Unbounded loop on user-controlled storage
If the loop bound comes from a mapping or array that users can grow (by calling addPlayer, addVoter, addRecipient), the loop will eventually hit the block gas limit as the list grows. The function becomes permanently uncallable. Fix: cap the list at a safe constant (e.g. 200) or use pagination.
Reading arr.length in the condition every iteration
Writing for (uint256 i = 0; i < arr.length; ++i) reads arr.length from storage on every iteration. Cache it first: uint256 len = arr.length. On a 100-element loop this saves roughly 9,700 gas (97 gas per SLOAD x 100 reads avoided).
Using i++ instead of ++i
Postfix increment i++ returns a copy of i before incrementing, which the compiler discards. The copy costs 3 extra gas per iteration. Always write ++i in for loop post statements. On a 1,000-iteration loop this saves 3,000 gas.
Deleting inside a loop without adjusting the index
Calling delete arr[i] inside a for loop over the same array resets the slot to zero but does not remove the element or change arr.length. The index still increments normally. If you want to remove elements while iterating, collect indices first, then remove in a second pass, or use the swap and pop pattern.
Section 08 · FAQ
Frequently asked questions
Answers to the most common questions about for loops in Solidity.
What is the difference between for and while loops in Solidity?
A for loop packs the initializer, condition, and post statement into one header line and is natural when you know the bound before the loop. A while loop has only a condition and is natural for open-ended repetition where the exit point emerges during execution. Both compile to identical EVM bytecode for equivalent logic, so the choice is about readability.
Can I have a for loop without any of the three parts?
Yes. All three parts are optional. Omitting the initializer leaves the loop variable in its outer scope. Omitting the condition creates an infinite loop that requires a break to exit. Omitting the post statement means you must advance the counter manually inside the body. The pattern for (uint256 i = 0; i < len; ) with unchecked increment in the body is common in gas-optimized production code.
How many iterations can a Solidity for loop safely do?
It depends on what is in the body. A loop that only reads from memory and does a small arithmetic operation can safely run several thousand iterations per transaction. A loop that does one SSTORE per iteration is limited to roughly 150 to 200 iterations before the transaction approaches the block gas limit of 30 million gas. Profile with a gas estimator before deploying any loop over more than 50 elements.
Is it safe to use a for loop inside a view function?
View functions do not pay gas when called externally (off chain). However, if another contract calls your view function on chain, it does consume gas. The real risk with view function loops is off chain calls that time out because the computation takes too long, or on chain calls that exceed the gas cap. For view functions accessed by other contracts on chain, the same loop size limits apply.
What does unchecked do inside a for loop?
The unchecked block disables the overflow and underflow checks that Solidity 0.8 adds to every arithmetic operation by default. Inside a loop counter, the overflow check on ++i costs roughly 30 gas per iteration and is almost always unnecessary because a counter incrementing by 1 per iteration will never overflow uint256. Wrapping the increment in unchecked removes this overhead safely when overflow is provably impossible.
How do I loop through an array in Solidity?
Use a for loop with the array length as the bound: for (uint256 i = 0; i < arr.length; ++i) { do something with arr[i]; }. For gas optimization, cache the length in a local variable before the loop and wrap ++i in an unchecked block. Always check that the array is bounded so the loop cannot exceed the block gas limit.
What is the gas cost of a for loop in Solidity?
An empty for loop iteration costs about 17 gas: 10 gas for the JUMPI plus 3 gas each for counter read, comparison, and increment. Body opcodes add to this base. A storage read inside the body adds 100 to 2,100 gas per iteration depending on whether the slot is warm or cold. Total cost is roughly base cost times iterations plus body cost per iteration.