What a constructor does

A constructor is a special function that runs only once: when the contract is deployed. Its purpose is to initialize storage variables, validate deployment parameters, and set up any state the contract needs before it can be used.

In modern Solidity, a constructor is declared with the constructor keyword and has no name.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Vault {
    address public owner;
    uint256 public feeBps;

    constructor(address _owner, uint256 _feeBps) {
        require(_owner != address(0), "owner is zero address");
        require(_feeBps <= 1000, "fee too high");

        owner = _owner;
        feeBps = _feeBps;
    }
}

When this contract is deployed, the constructor receives arguments, validates them, and stores them in contract state. After deployment, the constructor can never be called again.

Why constructors matter

Constructors help you:

  • enforce valid deployment parameters
  • avoid uninitialized state
  • reduce the need for post-deployment setup transactions
  • make contracts easier to reason about and audit
  • set immutable values that should never change

For example, if a contract must always know its administrator or treasury address, setting that value in the constructor is safer than relying on a later function call.


Constructor syntax and behavior

A constructor looks like a function, but it has important differences:

  • it has no name
  • it can be declared payable
  • it runs only during deployment
  • it cannot be called after deployment
  • it cannot return values

Basic syntax

pragma solidity ^0.8.20;

contract Example {
    uint256 public threshold;

    constructor(uint256 _threshold) {
        threshold = _threshold;
    }
}

Constructor visibility

Older Solidity versions allowed visibility specifiers such as public or internal on constructors. In current Solidity versions, constructor visibility is not used in the same way, and you should follow the syntax supported by your compiler version. For modern projects, the important point is that constructors are deployment-time only.

Payable constructors

If you want the contract to accept Ether during deployment, mark the constructor as payable.

pragma solidity ^0.8.20;

contract DepositBox {
    address public depositor;

    constructor() payable {
        depositor = msg.sender;
    }
}

If the constructor is not payable, deployment with value will fail.


Initializing state variables effectively

Constructors are often used to initialize storage variables, but not every value needs to be assigned inside the constructor. Solidity offers several initialization options.

Initialization methodWhen it runsBest for
Inline state variable assignmentAt deployment, before constructor bodyFixed defaults and constants
Constructor assignmentDuring deployment, in constructor bodyValues that depend on parameters or validation
Immutable variablesDuring deployment, then locked foreverAddresses, IDs, or settings that never change

Inline initialization

pragma solidity ^0.8.20;

contract Config {
    uint256 public maxUsers = 100;
}

This is simple and readable when the value is fixed.

Constructor initialization

pragma solidity ^0.8.20;

contract Config {
    uint256 public maxUsers;

    constructor(uint256 _maxUsers) {
        maxUsers = _maxUsers;
    }
}

Use this when the value is deployment-specific.

Immutable variables

Immutable variables are assigned once in the constructor and then become read-only.

pragma solidity ^0.8.20;

contract Token {
    address public immutable treasury;

    constructor(address _treasury) {
        require(_treasury != address(0), "invalid treasury");
        treasury = _treasury;
    }
}

Immutables are useful because they are cheaper to read than regular storage variables and cannot be changed later.


A practical example: deploying a simple fee collector

Suppose you want a contract that collects Ether and forwards a configurable fee to a treasury address. The constructor should establish the treasury and fee rate at deployment.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract FeeCollector {
    address public immutable treasury;
    uint256 public immutable feeBps;
    address public owner;

    constructor(address _treasury, uint256 _feeBps) {
        require(_treasury != address(0), "treasury is zero address");
        require(_feeBps <= 500, "fee exceeds 5%");

        treasury = _treasury;
        feeBps = _feeBps;
        owner = msg.sender;
    }

    function collect() external payable {
        require(msg.value > 0, "no ether sent");

        uint256 fee = (msg.value * feeBps) / 10_000;
        uint256 remainder = msg.value - fee;

        (bool ok1, ) = treasury.call{value: fee}("");
        require(ok1, "fee transfer failed");

        (bool ok2, ) = owner.call{value: remainder}("");
        require(ok2, "owner transfer failed");
    }
}

What this constructor achieves

  • prevents deployment with a zero treasury address
  • caps the fee to a reasonable range
  • stores critical values as immutables
  • assigns the deployer as the owner

This pattern is common in production contracts: validate deployment inputs early, then lock them in.


Constructors and inheritance

Inheritance is one of the most important reasons to understand constructors well. When a contract inherits from another contract, the parent constructor may require arguments too.

Passing arguments to a parent constructor

pragma solidity ^0.8.20;

contract OwnableBase {
    address public owner;

    constructor(address _owner) {
        require(_owner != address(0), "invalid owner");
        owner = _owner;
    }
}

contract ManagedVault is OwnableBase {
    uint256 public limit;

    constructor(address _owner, uint256 _limit) OwnableBase(_owner) {
        limit = _limit;
    }
}

The derived contract calls the parent constructor in the inheritance list after the constructor signature.

Multiple inheritance

If a contract inherits from multiple parents, each parent constructor may need its own arguments. The order and syntax matter, and the compiler will enforce correct initialization.

pragma solidity ^0.8.20;

contract A {
    uint256 public a;
    constructor(uint256 _a) { a = _a; }
}

contract B {
    uint256 public b;
    constructor(uint256 _b) { b = _b; }
}

contract C is A, B {
    constructor(uint256 _a, uint256 _b) A(_a) B(_b) {}
}

Best practice for inherited constructors

  • keep parent constructors small and focused
  • validate parent inputs in the parent contract when possible
  • avoid duplicated initialization logic across child contracts
  • document the expected constructor arguments clearly

This makes deployment easier and reduces the chance of misconfiguration.


Constructor patterns you will use often

1. Ownership initialization

Many contracts need an owner or admin set at deployment.

constructor() {
    owner = msg.sender;
}

This is the simplest pattern, but it assumes the deployer should control the contract. If the owner should be a multisig or another address, pass it explicitly.

2. Dependency injection

If your contract depends on another contract, pass its address into the constructor.

constructor(address _priceOracle) {
    require(_priceOracle != address(0), "oracle is zero address");
    priceOracle = _priceOracle;
}

This is common for oracles, registries, routers, and token contracts.

3. Parameter validation

Always validate constructor inputs that could break contract behavior.

constructor(uint256 _minDeposit, uint256 _maxDeposit) {
    require(_minDeposit > 0, "min deposit must be positive");
    require(_minDeposit <= _maxDeposit, "invalid range");
    minDeposit = _minDeposit;
    maxDeposit = _maxDeposit;
}

4. Precomputing configuration

If a value is expensive or awkward to compute repeatedly, calculate it once in the constructor.

constructor(uint256 _scale) {
    scale = _scale;
    precision = 10 ** _scale;
}

This can simplify later logic and reduce runtime gas costs.


Common mistakes to avoid

Forgetting to validate zero addresses

A zero address often causes irreversible deployment mistakes.

require(_recipient != address(0), "recipient is zero address");

Relying on the deployer when that is not intended

msg.sender in the constructor is the deploying account or contract. That is not always the correct owner. If deployment is done through a factory or script, the deployer may not be the intended administrator.

Leaving critical values mutable unnecessarily

If a value should never change, make it immutable or constant instead of storing it in mutable state.

Doing too much work in the constructor

Constructors should initialize, not perform heavy operational logic. Avoid:

  • large loops over unbounded data
  • external calls unless absolutely necessary
  • complex setup that can fail unpredictably

Heavy constructors can make deployment expensive and fragile.

Assuming constructor code can be reused

A constructor is not a setup function. If you need post-deployment reconfiguration, create an explicit initializer function and protect it carefully. For standard contracts, constructor logic should be final and one-time only.


Constructor vs initializer: when to use each

Some projects use initializer functions instead of constructors, especially when contracts are deployed behind proxies. For a regular Solidity contract, the constructor is usually the right choice.

ScenarioPrefer constructorPrefer initializer
Standard direct deploymentYesNo
Immutable deployment settingsYesNo
Proxy-based upgradeable patternNoYes
One-time setup after clone deploymentSometimesOften

For getting started with Solidity, the constructor is the simplest and safest tool when the contract is deployed directly and its state should be fixed at creation time.


Deployment tips for constructors

Keep constructor arguments explicit

Prefer passing all required settings into the constructor rather than hardcoding them. This makes deployments reproducible across testnets and mainnet.

Document expected values

If a constructor accepts addresses or numeric limits, document what each parameter means and any valid range. This is especially important for deployment scripts and audits.

Test edge cases

Even if you are not writing a full test suite yet, manually verify:

  • zero address rejection
  • boundary values for fees and limits
  • correct ownership assignment
  • inherited constructor arguments

Use named deployment variables in scripts

When deploying from scripts, use descriptive variable names so constructor arguments are easy to review.

const treasury = "0x1234...abcd";
const feeBps = 250;

This reduces the risk of accidentally swapping arguments.


Summary

Constructors are the foundation of safe Solidity contract initialization. They let you set required state once, validate deployment parameters, and lock in important configuration such as owners, treasury addresses, and fee settings. They are especially valuable when combined with immutable variables and inheritance-aware design.

A good constructor is small, explicit, and defensive: it initializes only what must be known at deployment, rejects invalid inputs, and leaves the contract ready for use immediately after deployment.

Learn more with useful resources