Skip to content

Migrate your project to ESM

Hardhat 3 is ESM-first, so your project needs to be an ESM package. This guide walks you through converting an existing CommonJS project to ESM, and shows the escape hatches for keeping individual files as CommonJS when that’s easier.

Add "type": "module" to your package.json:

Terminal window
npm pkg set type=module

From this point on, every .js file in your project is treated as an ES module.

If you have a tsconfig.json, set compilerOptions.module to an ESM-compatible value like "node20":

{
"compilerOptions": {
"module": "node20"
}
}

This guide covers the conversion in depth, but the rules you’ll hit most often are:

  • Replace const x = require("x") with import x from "x".
  • When importing a relative path, you must include the file extension. const x = require("./x") becomes import x from "./x.js", not import x from "./x".
  • Replace module.exports = x with export default x, and module.exports.x = 1 with export const x = 1.
  • __dirname and __filename are now import.meta.dirname and import.meta.filename.

For example, a CommonJS script like this:

const path = require("path");
const helper = require("./helper");
module.exports = function run() {
console.log(path.join(__dirname, helper.name));
};

becomes:

import path from "path";
import helper from "./helper.js";
export default function run() {
console.log(path.join(import.meta.dirname, helper.name));
}

If converting a file is impractical, rename it from .js to .cjs. CommonJS files keep working alongside ESM ones, and you don’t have to change their syntax.

We still recommend writing new files in ESM to take advantage of modern JavaScript features and match Hardhat 3’s defaults.

You can’t require("hardhat") at the top level of a .cjs file, because Hardhat is an ES module. Use a dynamic import from inside an async function instead:

async function main() {
const { default: hre } = await import("hardhat");
// use hre
}
main();

If you’re using Hardhat from a Mocha test, the same pattern applies inside a before hook. See the Mocha tests migration guide for an example.

If a Hardhat command failed with one of the errors below, here’s what’s happening and how to fix it.

__dirname is not defined in ES module scope

Section titled “__dirname is not defined in ES module scope”

A file is now being treated as ESM, but it still uses the CommonJS-only __dirname (or __filename).

Replace those with import.meta.dirname and import.meta.filename. See Convert source files to ESM for the other syntax differences you may need to address.

Cannot use import statement outside a module

Section titled “Cannot use import statement outside a module”

Node.js is loading a file as CommonJS, but the file uses import syntax.

Either set "type": "module" in package.json so .js files are loaded as ESM (see Set your project to ESM), or rename the file to .mjs.

A CommonJS file is trying to require() an ES module. The most common case is require("hardhat") from a .cjs file.

Convert the file that’s calling require() to ESM, or load the ES module with a dynamic await import(). See Using Hardhat from a CommonJS file for the pattern.