BlockchainSoliditySmart Contracts13 min readUpdated

while Loop in Solidity: Binary Search and Sentinel Patterns

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: while Loop in Solidity: Binary Search and Sentinel Patterns

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.

solidity
// 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-maker

The 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.

Two-column comparison showing a for loop on the left with initializer, condition, and post statement labeled, and an equivalent while loop on the right with the initializer moved outside and post statement moved inside the body.
Every for loop can be rewritten as a while loop. Use for when the bound is a simple counter, while when the exit condition is more complex.

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 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.

solidity
// 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.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View agentic AI consulting serviceSee ChainTrust case study

Related service

Agentic AI Consulting

See scope & pricing →

Related case study

ChainTrust Compliance Engine

Read case study →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →