Debugging Solidity with Hardhat's console.log: How It Works and When to Remove It
Hardhat's console.log brings print-style debugging to Solidity. Here's how it intercepts calls on the Hardhat Network, a working example, and why you must strip it before deploying.
09 Aug 2025, 03:06 UTC

The problem: Solidity has no print statement
If you come from JavaScript or Python, your first instinct when a contract misbehaves is to print a variable. Solidity gives you no such thing. Events exist, but they require defining an event, emitting it, and then decoding logs from a transaction receipt — a lot of ceremony to answer "what is amount right now?"
Hardhat solves this with a development-only console.log that works inside Solidity. Import one file, call it like you would in JavaScript, and the output appears in your terminal while tests or a local node run. This post covers how it actually works, a concrete example, and the two mistakes people make with it: leaving it in production code and expecting it to work on live networks.
How console.log works under the hood
When you import hardhat/console.sol, your contract gains access to a library of console.log overloads (for strings, uints, addresses, booleans, and combinations thereof). These compile down to calls at a special address that the Hardhat Network — Hardhat's built-in local EVM — recognizes and intercepts. Instead of executing the call as normal bytecode, the network decodes the arguments and prints them to your terminal in execution order, interleaved with transaction hashes and other Hardhat output.
Two consequences follow from this design. First, it only works where Hardhat controls the EVM: the in-process Hardhat Network, a local npx hardhat node, and mainnet forks. It does nothing useful on Sepolia, mainnet, or any network reached through a third-party RPC provider like Infura or Alchemy — at best the call is silently meaningless, at worst it wastes gas on a call to an address with no special handling. Second, each log is a real EVM call, so it costs gas and increases bytecode size. Fine for development; unacceptable for deployment.
A worked example
Assuming a standard Hardhat project (Hardhat 2.x, created with npx hardhat init), here is a minimal contract:
// contracts/Counter.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract Counter {
uint256 public count;
function increment(uint256 by) external {
console.log("increment called by:", msg.sender);
console.log("count before:", count);
count += by;
console.log("count after:", count);
}
}And a test that exercises it:
// test/Counter.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Counter", function () {
it("logs values during increment", async function () {
const Counter = await ethers.getContractFactory("Counter");
const counter = await Counter.deploy();
await counter.increment(5);
expect(await counter.count()).to.equal(5);
});
});Run it from the project root with npx hardhat test. No special permissions are needed; everything runs locally. In the terminal you should see the three log lines printed in order, alongside the test results — the sender address, then count before: 0, then count after: 5. If the logs do not appear, the most common causes are that the import path is wrong (it must be exactly hardhat/console.sol) or that your test is pointed at a non-Hardhat network via --network.
You can also see logs live by running npx hardhat node in one terminal and executing a script against it with npx hardhat run scripts/your-script.js --network localhost in another. The node process prints the logs as transactions execute.
Trade-offs and limitations
The feature is deliberately crude. There is no log level, no filtering, and no structured output — every call prints every time. In a test suite with many transactions, the output can bury the information you actually want, so treat logs as temporary scaffolding rather than permanent instrumentation.
The more serious risk is deployment. Because each console.log is an EVM call, it adds runtime gas and deployment bytecode cost, and it can leak internal state to anyone reading your source. The call will not revert on a live network in most configurations, but it accomplishes nothing there. The practical rule: strip all console.sol imports before deploying. A simple pre-deployment check is to grep for it:
# Run from the project root; should print nothing before a release
grep -rn "hardhat/console.sol" contracts/Some teams enforce this with a CI step or a lint rule so a stray import cannot reach a tagged release.
Verifying it yourself
The fastest sanity check is the example above: create the contract, run npx hardhat test, and confirm the log lines appear in the terminal. If you prefer an interactive session, npx hardhat console connects to the in-process Hardhat Network, where you can deploy and call the contract and watch logs print immediately. The Hardhat documentation under "Hardhat Network → console.log" lists the supported argument types and overloads if you need to log something beyond the basic types.
The takeaway: console.log is the fastest feedback loop Hardhat offers for contract debugging, but it is a development tool with a hard boundary — Hardhat-controlled networks only, and out of the codebase before anything ships.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.