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.
Section 07 · FAQ
Frequently asked questions
Answers to the most common questions about while loops in Solidity.
What is a while loop in Solidity in simple words?
A while loop runs a block of code over and over as long as a condition stays true. You write while (condition) followed by curly braces. The condition is checked before every run, including the first. If the condition is false before the loop starts, the body never runs at all.
What happens if a while loop runs forever in Solidity?
The transaction consumes all the gas provided by the caller and reverts with an out-of-gas error. No state changes are saved. The caller loses the gas fee. In practice this happens when the exit condition is never reached because the state variable in the condition is never updated inside the body.
How is a while loop different from a for loop in Solidity?
A for loop has an initializer and a post statement built into its header, making it natural for counter-driven iteration over a known bound. A while loop has only the condition, so you manage the counter manually. Both compile to identical EVM bytecode for equivalent logic. The difference is purely about readability and which shape fits the algorithm.
Can I use break and continue inside a while loop?
Yes. break exits the while loop immediately, skipping any remaining body and the condition check. continue jumps back to the condition check immediately, skipping the rest of the current body. Both work identically in while and for loops. Be careful with continue: if you use continue before the statement that advances the counter, the loop becomes infinite.
Is while (true) safe in Solidity?
Only if every code path through the body reaches a break before running out of gas. In practice, while (true) with a break is rarer in Solidity than in general programming because any bug that prevents the break from firing causes a gas exhaustion revert. Most Solidity developers prefer a bounded for loop with an early break, which makes the maximum iteration count visible.
When should I use a while loop over a for loop in Solidity?
Use a while loop when the number of iterations is not known up front and depends on runtime state. Classic cases are binary search, sentinel search, and walking a linked list. Use a for loop when you have a clear counter from zero to length. Both compile to identical bytecode for equivalent logic, so the choice is about which shape reads better for the algorithm.
How do I write a safe infinite while loop in Solidity?
There is no safe infinite loop in Solidity. Every loop must have an exit path because all execution costs gas and the block gas limit will eventually halt anything that runs forever. If you need open ended iteration, set a hard MAX iteration counter and break out when reached, or move the loop off chain and submit the result back through a transaction.