What a Modifier Is
A function modifier is a reusable block of code that runs before and/or after a function body, most often to enforce a precondition such as 'only the owner may call this' or 'the sale must still be open'. You declare it with the modifier keyword and place a special underscore _; where the wrapped function's body should execute. Applying a modifier to a function is as simple as listing its name in the function header. Modifiers exist to eliminate repeated guard code and to make security constraints declarative and easy to audit.
Cricket analogy: A modifier is like the umpire's no-ball check that runs before every legal delivery is counted — the _; is the moment the actual ball is bowled, gated by the pre-check.
The Underscore Placeholder and Execution Order
The _; inside a modifier marks where the modified function's body is inlined. Code before _; runs first (typically the checks), then the function body executes, then any code after _; runs. You can even place logic after _; for post-conditions, and a modifier may contain _; only once in a straight-line body. When multiple modifiers are applied to a function, they execute left-to-right in the order listed, nesting like layers of an onion — the first modifier's pre-code runs first and its post-code runs last.
Cricket analogy: Applying two modifiers is like two successive reviews — the front-foot no-ball check wraps the wide check, and they resolve outermost-first then innermost, just as pre-code runs left-to-right.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract Sale {
address public owner;
uint256 public deadline;
bool private locked;
constructor(uint256 duration) {
owner = msg.sender;
deadline = block.timestamp + duration;
}
// Access control modifier with a parameter-free check
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_; // function body runs here
}
// Modifier taking an argument
modifier before(uint256 time) {
require(block.timestamp < time, "too late");
_;
}
// Reentrancy guard: code before AND after the placeholder
modifier nonReentrant() {
require(!locked, "reentrant call");
locked = true;
_;
locked = false; // post-condition cleanup
}
// Multiple modifiers run left-to-right
function buy() external before(deadline) nonReentrant {
// purchase logic here
}
function setOwner(address next) external onlyOwner {
owner = next;
}
}Modifiers can accept arguments, as in before(deadline). The arguments are evaluated when the function is called. This lets one modifier definition enforce different thresholds across many functions, keeping the guard logic in a single, auditable place.
Best Practices and Pitfalls
Keep modifiers small and focused on checks; complex logic belongs in the function body or an internal function, because deeply nested modifier logic is hard to audit and can obscure control flow. A common guideline is that modifiers should mainly perform require checks and simple state toggles, as with a reentrancy guard. Because a modifier without a reachable _; will prevent the function body from ever running, an accidental early return or a missing _; in a branch is a subtle and dangerous bug. Widely used libraries such as OpenZeppelin's Ownable and ReentrancyGuard provide battle-tested modifiers you should prefer over hand-rolled ones.
Cricket analogy: A good modifier is a crisp lbw check, not a whole coaching session — overload it and, like an umpire adjudicating strategy mid-over, it muddies the game's flow.
A modifier that can reach a code path with no _; will silently skip the function body, and a modifier that reverts under the wrong condition can lock a function forever. Always ensure _; is reachable on every intended path, and never put untrusted external calls before _; in an access-control modifier.
Under the hood, modifiers are inlined into each function they decorate rather than being separate function calls, so reusing one modifier across ten functions increases deployed bytecode size (each call site gets a copy). When bytecode size or deployment gas is a concern, a common pattern is to have the modifier delegate to a small internal function — for example modifier onlyOwner() { _checkOwner(); _; } — so the heavy logic lives once and only a tiny call is inlined at each site. This is exactly the pattern OpenZeppelin adopted in later versions of Ownable.
Cricket analogy: Inlining a modifier ten times is like printing the full rulebook on ten scorecards; instead you reference one central rulebook (_checkOwner) and each card just points to it.
- A modifier is reusable code that wraps a function to enforce preconditions or post-conditions.
- The
_;placeholder marks where the wrapped function body executes; pre-code runs before, post-code after. - Multiple modifiers execute left-to-right and nest like an onion, unwinding in reverse order.
- Modifiers can take arguments, letting one definition enforce different thresholds across functions.
- Keep modifiers small — mostly require checks and simple toggles like a reentrancy guard.
- An unreachable
_;silently skips the function body; ensure_;is reachable on every path. - Modifiers are inlined, increasing bytecode; delegate heavy logic to an internal function, as OpenZeppelin does.
Practice what you learned
1. What does the `_;` symbol represent inside a modifier?
2. If a function is declared as `function f() external modA modB { ... }`, in what order does the pre-code run?
3. Why is a reentrancy guard modifier able to lock and unlock around the function body?
4. What is a risk of a modifier that has a code path where `_;` is not reached?
5. Why does the OpenZeppelin pattern have onlyOwner call an internal _checkOwner() function?
Was this page helpful?
You May Also Like
Functions and Visibility Modifiers
Understand how Solidity functions are declared and the four visibility levels — public, external, internal, and private — plus state mutability keywords like view and pure.
Error Handling with require, revert, assert
Solidity handles errors by reverting state changes. Learn when to use require for input validation, revert with custom errors for gas-efficient failures, and assert for invariants that must never fail.
Conditionals and Loops in Solidity
Learn how Solidity handles branching with if/else and repetition with for, while, and do-while loops, and why unbounded loops are dangerous in a gas-metered environment.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics