Section 01 · Definition
What is a while loop in Solidity?
A while loop repeats a block of code as long as a boolean condition stays true. Unlike a for loop, it has no initializer or post statement in its header — only the condition.
Quick answer
In one sentence: A Solidity while loop evaluates a condition before every iteration and continues running the body as long as that condition is true, making it the right choice when the number of iterations is not known until execution time.
The minimal while loop has one line of header and a body. The programmer is responsible for both setting up any counter or state variable before the loop and advancing it inside the body. Forgetting to advance the state is the most common while loop bug — the condition never becomes false and the loop runs until it consumes all gas.
// Example 1 — countdown from 5 to 1
uint256 count = 5;
while (count > 0) {
count--; // runs: 5, 4, 3, 2, 1 → exits when count = 0
}
// Example 2 — find the first score that passes (>= 50)
uint256[] memory scores; // e.g. [30, 45, 72, 88]
uint256 i = 0;
while (i < scores.length && scores[i] < 50) {
i++; // skips 30 and 45
}
// after loop: i = 2 (scores[2] = 72, first passing score)
// Example 3 — follow a delegation chain in governance
mapping(address => address) storage delegatedTo;
address current = msg.sender;
while (delegatedTo[current] != address(0)) {
current = delegatedTo[current]; // follow until end of chain
}
// current is now the final decision-makerThe key rule for every while loop: the variable checked in the condition must change inside the body. If it never changes, the condition stays true forever and the transaction runs out of gas. In all three examples above, each iteration either decrements count, advances i, or moves current forward — so the loop always makes progress toward exit.
Section 02 · Comparison
while vs for: when to use each
The two loops compile to identical EVM bytecode for equivalent logic. The choice is about which shape expresses your intent more clearly.
Use for when the bound is a counter
If you know you need exactly arr.length iterations and the counter increments by one each time, a for loop expresses that directly. The initializer, condition, and post statement appear together in one line, making the bounds visible at a glance.
Use while when the exit emerges during execution
Binary search, convergence algorithms, and state machine transitions all have exit points that depend on what is discovered at runtime. A while loop is natural here because the condition is not reducible to a simple counter. The bound is still required — it is just more complex.
Both are equivalent in gas
A for loop compiled to EVM bytecode is identical to the equivalent while loop. The compiler rewrites them to the same JUMPI instruction. The difference is only at the source level. Pick whichever is easier to read for the specific algorithm.
Section 03 · Canonical pattern
Binary search: the classic while loop use case
Binary search is the textbook reason to use while over for. The number of iterations is not known in advance — it depends on where the target sits in the sorted array.
/// @notice Binary search over a sorted uint256 array.
/// @return index The position of target, or type(uint256).max if not found.
function binarySearch(
uint256[] storage arr,
uint256 target
) internal view returns (uint256 index) {
if (arr.length == 0) return type(uint256).max;
uint256 left = 0;
uint256 right = arr.length - 1;
while (left <= right) {
// Overflow-safe midpoint: avoids (left + right) overflow
uint256 mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
// Prevent underflow when right would go below zero
if (mid == 0) break;
right = mid - 1;
}
}
return type(uint256).max; // not found
}The loop runs at most log₂(n) iterations, so a sorted array of 1,000 elements needs at most 10 iterations. That is safe even inside a transaction. The key property that makes a while loop valid here: on every iteration either left increases or right decreases, so the loop always makes progress toward the exit condition.
Why (left + right) / 2 can overflow
If left and right are both close to type(uint256).max, their sum overflows. The safe version is left + (right - left) / 2. Since right >= left by construction, the subtraction never underflows. This is a classic bug in binary search implementations across all languages.
Section 04 · Full contract
Complete example: GovDelegate smart contract
A governance delegation contract where token holders delegate their voting power to another address. A while loop follows the delegation chain to find the final voter. This is the same pattern used by OpenZeppelin Governor. Deploy on Remix and test the chain-following logic.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
/// @title GovDelegate
/// @notice Governance contract with vote delegation.
/// A while loop follows the delegation chain to find
/// the final effective voter for any address.
/// Chain depth is capped at MAX_DEPTH to prevent gas exhaustion.
contract GovDelegate {
// ── Constants ────────────────────────────────────────────────────
uint256 public constant MAX_DEPTH = 10; // max delegation hops
// ── State ────────────────────────────────────────────────────────
address public owner;
struct Voter {
uint256 weight; // voting power (set by owner)
bool voted; // has this address cast a final vote?
uint256 vote; // proposal index voted for
}
mapping(address => Voter) public voters;
mapping(address => address) public delegatedTo; // addr => delegate
uint256[] public proposalVotes; // vote count per proposal index
// ── Events ───────────────────────────────────────────────────────
event WeightGranted(address indexed voter, uint256 weight);
event Delegated(address indexed from, address indexed to);
event VoteCast(address indexed effectiveVoter, uint256 proposalIndex, uint256 weight);
// ── Modifiers ────────────────────────────────────────────────────
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
constructor(uint256 _proposalCount) {
owner = msg.sender;
for (uint256 i = 0; i < _proposalCount; ) {
proposalVotes.push(0);
unchecked { ++i; }
}
}
// ── Write ────────────────────────────────────────────────────────
/// Grant voting weight to an address (owner only).
function grantWeight(address _voter, uint256 _weight) external onlyOwner {
require(_voter != address(0), "zero address");
voters[_voter].weight = _weight;
emit WeightGranted(_voter, _weight);
}
/// Delegate your vote to another address.
/// The target may also have delegated — the while loop follows the chain.
function delegate(address _to) external {
require(_to != address(0), "zero address");
require(_to != msg.sender, "self-delegation");
require(!voters[msg.sender].voted, "already voted");
delegatedTo[msg.sender] = _to;
emit Delegated(msg.sender, _to);
}
/// Cast a vote. The while loop resolves any delegation chain first.
function castVote(uint256 _proposalIndex) external {
require(_proposalIndex < proposalVotes.length, "invalid proposal");
// ── While loop: follow the delegation chain ─────────────────
// Start at msg.sender and hop until we reach someone who
// has not delegated further (delegatedTo == address(0)).
address effectiveVoter = msg.sender;
uint256 hops = 0;
while (delegatedTo[effectiveVoter] != address(0)) {
effectiveVoter = delegatedTo[effectiveVoter];
unchecked { ++hops; }
require(hops <= MAX_DEPTH, "delegation chain too long");
}
// ────────────────────────────────────────────────────────────
require(!voters[effectiveVoter].voted, "effective voter already voted");
require(voters[effectiveVoter].weight > 0, "no voting weight");
voters[effectiveVoter].voted = true;
voters[effectiveVoter].vote = _proposalIndex;
proposalVotes[_proposalIndex] += voters[effectiveVoter].weight;
emit VoteCast(effectiveVoter, _proposalIndex, voters[effectiveVoter].weight);
}
// ── Read ─────────────────────────────────────────────────────────
/// Resolve the delegation chain for any address without spending gas.
/// Returns the final effective voter and how many hops the chain has.
function resolveDelegate(address _voter)
external view
returns (address effectiveVoter, uint256 hops)
{
effectiveVoter = _voter;
while (delegatedTo[effectiveVoter] != address(0)) {
effectiveVoter = delegatedTo[effectiveVoter];
unchecked { ++hops; }
if (hops > MAX_DEPTH) break; // safety cap for view calls
}
}
/// Return the proposal index with the most votes.
function winningProposal()
external view
returns (uint256 index, uint256 voteCount)
{
uint256 len = proposalVotes.length;
for (uint256 i = 0; i < len; ) {
if (proposalVotes[i] > voteCount) {
voteCount = proposalVotes[i];
index = i;
}
unchecked { ++i; }
}
}
/// Return all proposal vote counts at once.
function allProposalVotes() external view returns (uint256[] memory) {
return proposalVotes;
}
}To test the chain in Remix: deploy with 3 proposals, grant weight to address A, have A delegate to B, and B delegate to C. Call resolveDelegate(A) — it returns C with 2 hops. Then call castVote(0) from address A — the while loop follows A → B → C automatically and records the vote under C's weight.
Section 05 · Common mistakes
Four while loop mistakes to avoid
While loops are more error prone than for loops because you manage the counter manually. These four mistakes appear in real audits.
Forgetting to advance the state variable
If the variable in the condition never changes inside the body, the condition is always true and the loop runs until it exhausts gas, causing a revert. Every while loop must modify the condition variable or reach a break on every code path through the body.
Condition depends on user-controlled storage length
while (i < users.length) where users is a storage array that anyone can push to is the canonical unbounded loop. As users grows past the block gas limit, the function becomes permanently uncallable. Always cap list sizes with a MAX constant.
No fallback for the not-found case in search loops
A search while loop that returns nothing when the target is absent will cause a revert or return a default zero value that looks valid. Always return a sentinel value like type(uint256).max for not-found, or return a bool found alongside the result.
Underflow in a descending while loop
while (i >= 0) with a uint256 counter is an infinite loop because uint256 is always >= 0 and decrementing past zero underflows to type(uint256).max. For a descending loop over a uint256 index, loop while (i > 0) and process index i-1, or use a for loop from length to 1 with unchecked decrement.