Fix ERR_REQUIRE_ESM in gulp: ESM-only plugins, old gulp-cli
Why renaming to gulpfile.mjs still fails on Node 24 with gulp 4, what changes on newer Node, and three fixes reproduced in Docker.
TL;DR#
gulp-imagemin became pure ESM in 8.0.0, so a CommonJS gulpfile.js that calls require('gulp-imagemin') stops with ERR_REQUIRE_ESM on older Node. The durable fix is gulp-specific: rename the file to gulpfile.mjs, switch to import, and run gulp 5.0.1, whose CLI (gulp-cli 3.1.0) can load an ES-module gulpfile on current Node. Renaming alone is not enough on Node 24 with gulp 4: its CLI hits a second error, ERR_REQUIRE_ASYNC_MODULE, and gives up. If you cannot migrate today, pin gulp-imagemin@7.1.0, the last CommonJS release.
The Stack Overflow question shows the setup: Node 17.0.1, gulp 4.0.2, gulp CLI 2.3.0, and a gulpfile that requires seven plugins. Here is the same failure, reproduced on 10 October 2026 with gulp 4.0.2 and gulp-imagemin 9.2.0 in the official node:20.18.3-alpine Docker image:
Error [ERR_REQUIRE_ESM]: require() of ES Module /app/node_modules/gulp-imagemin/index.js from /app/gulpfile.js not supported.
Instead change the require of index.js in /app/gulpfile.js to a dynamic import() which is available in all CommonJS modules.The same gulpfile on Node v24.21.0 fails with a different message:
Error [ERR_REQUIRE_ASYNC_MODULE]: require() cannot be used on an ESM graph with top-level await. Use import() instead. To see where the top-level await comes from, use --experimental-print-required-tla.
Required module: /app/node_modules/gulp-imagemin/index.jsCheck your own error#
Paste the full error and the top of the stack trace to see which file is being loaded as which module type, and why Node refused it. It runs in your browser, nothing is sent.
Runs entirely in your browser — nothing you paste is sent anywhere. Tokens and passwords that appear in paths or URLs are masked in the output.
Optional: package.json, file extension, Node.js version (sharper advice)
Why it happens: two loaders, not one#
There are two places where a gulp run can trip over ES modules, and only the first one is the generic Node rule.
Your gulpfile requires an ESM-only plugin. The gulp-imagemin v8.0.0 release notes say it plainly: "This package is now pure ESM." The npm registry shows "type": "module" on every release from 8.0.0, published in August 2021, three months before the question. Before Node could require() ES modules, that alone produced ERR_REQUIRE_ESM. The general mechanism (how Node decides per file whether it is CommonJS or ESM) is covered in the exports is not defined in ES module scope fix; this page stays on what gulp adds.
gulp-cli loads the gulpfile itself. The gulp command does not run your file with node. It hands the path to a small loader, require-or-import.js. In gulp-cli 2.3.0, the version bundled with gulp 4, that loader calls require(path) first and only falls back to import() when the error code is exactly ERR_REQUIRE_ESM:
function requireOrImport(path, callback) {
var err = null;
var cjs;
try {
cjs = require(path);
} catch (e) {
if (pathToFileURL && importESM && e.code === 'ERR_REQUIRE_ESM') {
// This is needed on Windows, because import() fails if providing a Windows file path.
var url = pathToFileURL(path);
importESM(url).then(function(esm) { callback(null, esm); }, callback);
return;
}
err = e;
}
process.nextTick(function() { callback(err, cjs); });
}That was enough in 2021: requiring a gulpfile.mjs threw ERR_REQUIRE_ESM, the loader switched to import(), and the file loaded. Node then changed the rules. The Node modules documentation lists require() of ES modules as no longer behind a flag from v22.12.0 and v20.19.0, with one limit: "If the module being require()'d contains top-level await, or the module graph it imports contains top-level await, ERR_REQUIRE_ASYNC_MODULE will be thrown." The current gulp-imagemin index.js ends with four export const … = await exposePlugin(…) lines, which is top-level await.
So on Node 24, gulp-cli 2.3.0 requires your gulpfile.mjs, Node walks into gulp-imagemin, finds the top-level await, and throws ERR_REQUIRE_ASYNC_MODULE. The loader does not recognise that code and reports it as fatal. gulp-cli fixed this in PR #269, "Support top-level await on Node 22.12+", released as gulp-cli 3.1.0 in June 2025. The gulp 5.0.1 manifest depends on gulp-cli ^3.1.0.
Fix#
1. Upgrade gulp, then check which CLI actually runs#
npm install --save-dev gulp@5.0.1
npx gulp --versionThe output must show the 3.1.0 CLI:
CLI version: 3.1.0
Local version: 5.0.1The question's environment had a global gulp CLI 2.3.0. The CLI version line is the one that loads your gulpfile, so if a global install still reports 2.3.0, run gulp through npx gulp or an npm script, which use the project's own copy.
2. Rename the gulpfile to gulpfile.mjs and switch to import#
The .mjs extension makes Node treat the file as an ES module without touching "type" in package.json, so your other .js tooling files stay CommonJS. With gulp 5, named imports work because the package now ships an ESM entry:
import { src, dest, watch, series } from 'gulp';
import imagemin, { mozjpeg, optipng } from 'gulp-imagemin';
export function optimizeimg() {
return src('src/img/*.{jpg,png}')
.pipe(imagemin([
mozjpeg({ quality: 80, progressive: true }),
optipng({ optimizationLevel: 2 })
]))
.pipe(dest('dist/img'));
}
export default series(optimizeimg);Two details from the question's gulpfile change in the move. imagemin.mozjpeg(...) and imagemin.optipng(...) no longer exist: the v8.0.0 release notes say "The bundled plugins are now named exports instead of being attached to the default export", hence the named import above. And exports.default = series(...) becomes export default series(...). The question also writes optiminzationLevel, a typo; the option in the gulp-imagemin readme is optimizationLevel.
With gulp 4.0.2, import { src, dest } from 'gulp' fails on its own, before any plugin is reached:
SyntaxError: The requested module 'gulp' does not provide an export named 'dest'If you must stay on gulp 4 for a while, use the default import, import gulp from 'gulp' then const { src, dest } = gulp;, which is what the second answer on the question does. That form loaded on Node 20.18.3 with gulp 4, but on Node 24 the gulp-cli 2.3.0 problem above still applies, so step 1 is not optional on current Node.
3. Or keep a CommonJS gulpfile and load the plugin with import()#
If the gulpfile is large and you want to migrate later, load only the ESM-only plugin with a dynamic import() in a task of its own, and keep the stream task synchronous. Do not await the import inside the stream task itself: an async task returns a promise, and gulp's async-done counts the task as finished when that promise resolves — to the stream object, before dest() has written anything. In the reproduction that version printed Finished 'default' before gulp-imagemin: Minified 0 images, so a later task in a series() would start too early. Split it instead:
const { src, dest, series } = require('gulp');
let imagemin;
async function loadImagemin() {
({ default: imagemin } = await import('gulp-imagemin'));
}
function optimizeimg() {
return src('src/img/*.{jpg,png}')
.pipe(imagemin())
.pipe(dest('dist/img'));
}
exports.default = series(loadImagemin, optimizeimg);On Node v24.21.0 with gulp 4.0.2 this ran in order:
[00:22:27] Starting 'loadImagemin'...
[00:22:58] Finished 'loadImagemin' after 31 s
[00:22:58] Starting 'optimizeimg'...
gulp-imagemin: Minified 0 images
[00:22:58] Finished 'optimizeimg' after 126 ms4. Stopgap: pin the last CommonJS release#
The accepted answer's fix still works. gulp-imagemin 7.1.0 is CommonJS, so the original require() gulpfile loads unchanged:
npm install --save-dev gulp-imagemin@7.1.0It is a 2020 release. Pinning it keeps you on its old dependency tree, so treat it as a way to ship today, not as the fix.
Verify the fix#
Run the default task and look for the Using gulpfile line, which names the file gulp-cli actually loaded:
[22:42:10] Using gulpfile /app/gulpfile.mjs
[22:42:10] Starting 'default'...
gulp-imagemin: Minified 0 images
[22:42:10] Finished 'default' after 29 msThat run used gulp 5.0.1 and gulp-imagemin 9.2.0 on Node v24.21.0. The repro installed packages with --omit=optional --ignore-scripts and an empty src/img folder, so it proves the gulpfile loads and the task completes, not that images were compressed: run it on your real images to confirm the optimiser binaries are installed. The split import() variant (fix 3, gulp 4.0.2 on Node 24, with Minified printed before Finished 'optimizeimg') and the 7.1.0 pin (fix 4, under gulp 5.0.1, on both Node 20.18.3 and Node 24) also completed in the same setup. The repro called imagemin() without arguments; the mozjpeg and optipng named imports in fix 2 follow the release notes and readme, not the test run.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
Other gulp plugins that went ESM-only since this question#
A fresh npm install today pulls more ESM-only plugins than the question's 2021 tree did. Two you are likely to meet in a gulpfile:
- gulp-autoprefixer is ESM-only in its current 10.0.0 release, and has been since November 2023 according to the registry. On Node 20.18.3 it gives the same
ERR_REQUIRE_ESM. On Node 20.19.0 and Node 24 it does not throw at load time, because it has no top-level await:require()succeeds and returns the module namespace, and the task then fails:
[22:43:04] TypeError: prefix is not a functionThe Node documentation explains why: require() of an ES module returns the namespace object, "which places the default export in the .default property". The one-line change const prefix = require('gulp-autoprefixer').default; made the same gulpfile finish on Node 24. Importing it from gulpfile.mjs avoids the question entirely.
- del, often used for a
cleantask, went pure ESM in 7.0.0 and, per its release notes, moved from a default export to named exports:require('del')becomesimport {deleteAsync} from 'del'. Its latest release declares Node 22 or newer inengines.
A negative control is worth knowing too, because the question tried it: adding "type": "module" to package.json while keeping require() in gulpfile.js swaps one error for another on Node 24:
ReferenceError: require is not defined in ES module scope, you can use import insteadIf you rewrite the requires as imports but keep exports.default = …, the next message is exports is not defined in ES module scope, covered in that page. If a plugin's error mentions a subpath that is "not exported", the package's exports map is refusing a require() condition: see ERR_PACKAGE_PATH_NOT_EXPORTED. And since the question's gulpfile also uses gulp-sass, note that the old node-sass compiler it often pairs with is a native module tied to one Node version, which is the subject of the Node Sass binding fix.
The short version for a gulp project in 2026: the plugin went ESM years ago, and the gulp CLI only caught up with how current Node loads ESM in gulp-cli 3.1.0. Upgrade gulp so that CLI is the one running, move the gulpfile to .mjs, and import plugins by their named exports where their release notes say so.
Further reading: require() of ES Module string-width: fix yarn.lock in Docker.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
require() of ES Module string-width: fix yarn.lock in Docker
Why yarn install in a node:18 container dies in node-gyp with ERR_REQUIRE_ESM from wide-align, how to spot the broken yarn.lock entry, and two fixes reproduced in Docker.
Fix RangeError: Invalid time zone specified in Node.js
Find which of the three causes threw the error, from the value printed after the colon, and make toLocaleString and Intl.DateTimeFormat accept the zone again.
Fix pnpm Ignored Build Scripts for Native Dependencies
Reproduce a missing SQLite binding, approve only its installer, and keep the same permission working in CI without enabling every dependency script.


