The immediate fix is to stop using the CommonJS-only __dirname variable in your ESM Lambda handler. Derive the directory from the module URL instead:
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
This works with an .mjs handler and with JavaScript files treated as ESM by their nearest package.json. The error is a Node.js module-format issue, not a Puppeteer-specific failure. After applying it, separately verify your Lambda handler setting, packaged dependencies, Chromium binary, architecture and layer layout.
Why Lambda says “__dirname is not defined”
Node.js supports two module systems. CommonJS wraps each module and provides variables such as __dirname and __filename. ECMAScript modules (ESM) do not provide those wrapper variables. An ESM file that evaluates __dirname therefore throws a ReferenceError before Puppeteer can do anything.
A Lambda function can use either system. AWS console examples commonly use index.mjs, and AWS documents ESM handlers, so an ESM function can expose this error with Puppeteer, Playwright, or ordinary filesystem code. A community report using puppeteer-core 22.3.0 on Node 20.x shows the same URL-based replacement; it is an illustrative configuration, not a guarantee that every browser package will run unchanged (Stack Overflow example).
#1 Best Overall
How Node decides the module type
.mjsfiles are ESM..cjsfiles are CommonJS.- A nearest
package.jsonwith"type":"module"treats.jsas ESM;"type":"commonjs"treats them as CommonJS. - Ambiguous
.jsfiles may be inferred from syntax in recent Node releases, which is another reason to make the format explicit while debugging.
See Node’s module documentation for the precise rules (ES modules and packages and module type).
Fix an ESM handler with the compatible URL pattern
Put this at module scope in your handler or in the module that needs a filesystem directory:
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const handler = async () => {
const executablePath = path.join(__dirname, 'bin', 'chromium');
// Configure Puppeteer with executablePath as appropriate for your package.
return { statusCode: 200, body: 'Path resolved' };
};
import.meta.url identifies the current module as a file: URL. fileURLToPath converts it to the operating system path, and path.dirname returns the containing directory. This is the practical compatibility pattern when the deployed runtime might be older than the versions that expose import.meta.dirname (Node ESM documentation).
Use import.meta.dirname when the runtime supports it
Node documents import.meta.dirname as available beginning in Node 20.11 and 21.2. It became non-experimental in Node 22.16 and 24.0. If your Lambda runtime’s exact Node minor version supports it, the equivalent is:
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #2
const here = import.meta.dirname;
Check the function’s configured runtime before deploying this shorter form. “Node 20” is not a sufficient version statement when a feature was introduced during that release line. If you cannot control the minor version or want the broadest ESM compatibility, retain the fileURLToPath form. AWS runtime selection and configuration are described in its Lambda runtimes documentation.
Keep ESM imports correct around Puppeteer
ESM resolution differs from CommonJS. Use explicit relative extensions for local modules and avoid reaching into undocumented package internals:
import { launch } from 'puppeteer-core';
import { helper } from './helper.js';
Package exports declarations can block internal paths that happened to work under CommonJS. The __dirname replacement only resolves your own module directory; it does not make a Chromium executable, native dependency or Puppeteer build compatible with Lambda.
Alternative: convert the function deliberately to CommonJS
CommonJS is valid when the file is actually interpreted as CommonJS and the Lambda handler points to it. Rename the handler to index.cjs, or use a package configuration that makes the file CommonJS, then use the CommonJS conventions consistently:
const path = require('node:path');
const puppeteer = require('puppeteer-core');
const __dirname = path.dirname(__filename);
exports.handler = async (event) => {
const executablePath = path.join(__dirname, 'bin', 'chromium');
// Launch with the executable and flags required by your chosen build.
return { statusCode: 200, body: 'CommonJS handler loaded' };
};
Configure the Lambda handler as index.handler when the file is index.cjs. Do not simply replace import with require inside an ESM file: require is not defined there unless you intentionally construct it with module.createRequire(). AWS shows separate ESM and CommonJS handler forms in Building Lambda functions with Node.js.
Which option should you choose?
| Choice | Runtime requirement | Code/configuration change | Best fit |
|---|---|---|---|
| URL conversion | Works across earlier ESM runtimes | Add two imports and two constants | Existing .mjs or type: module projects |
import.meta.dirname |
Node 20.11+, 21.2+; verify exact Lambda minor version | One property | Projects controlling a supported runtime |
| CommonJS | File/package and handler must be CommonJS | Change module format and imports | Handlers and dependencies already built around require |
Deployment checklist after the ReferenceError is gone
- Confirm the handler identity. The setting must name the file and exported function you deployed. For an ESM
index.mjsexportinghandler, useindex.handler. For a CommonJSindex.cjswithexports.handler, use the same logical setting. - Place the handler at the ZIP root. In a ZIP deployment, the entry file must be at the archive root unless your handler path explicitly reflects a subdirectory.
- Package dependencies. Include Puppeteer,
puppeteer-core, and any libraries not supplied by the runtime in the ZIP or a Lambda layer. AWS documents a 250 MB unzipped ZIP limit including layers; verify the current limit if your browser bundle approaches it (ZIP package documentation). - Check layer layout. Node.js layers use
nodejs/node_modulesor a runtime-specific path such asnodejs/nodeXX/node_modules. Native and binary packages must be built for Linux and for the function’s architecture. - Verify the browser executable. Puppeteer still needs a Chromium binary that matches the selected package and Lambda environment, plus a launch configuration suitable for that binary. The directory fix does not validate executable location, flags, native libraries or architecture.
- Test the deployed artifact. A local invocation can pass while the ZIP omits a file or layer. Log the resolved path, test file existence, and inspect the CloudWatch error for the first failing operation.
Common failures and targeted fixes
The same ReferenceError remains
The affected file may be a different module from the one you edited, or Lambda may still be running an older ZIP. Search the deployed artifact for __dirname, confirm the publish step, and log import.meta.url temporarily in ESM.
“Cannot find module” after switching formats
Check the nearest package.json, file extension and import path. In ESM, add the .js extension to relative imports. In CommonJS, ensure the handler file is .cjs or the package type is explicitly CommonJS.
“require is not defined in ES module scope”
You have mixed syntaxes. Keep the module ESM and use import, or convert the complete handler to CommonJS. A partial conversion creates a second module-system error rather than fixing the first.
Rank #4
Chromium cannot launch
This is a separate deployment problem. Confirm that the executable exists at the path you construct, that its native libraries are present, and that the binary was built for the Lambda architecture. Review the selected Puppeteer package’s Lambda requirements instead of assuming the __dirname change fixed browser setup.
ZIP or layer size errors
Remove development files, put shared dependencies in a correctly structured layer, or choose a packaging approach that fits the documented unzipped limit. Recalculate the combined function and layer size, not just the local project directory.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
If your objective is simply a reliable screenshot or PDF rather than running Chromium inside your own Lambda package, ScreenshotNeo provides a website screenshot API and MCP server. A single GET request returns PNG, JPEG, WebP or PDF:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for parameters. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server supplies take_screenshot, get_page_info and capture_pdf tools to Claude, Cursor and other MCP clients.
The free plan includes 1,000 screenshots per month without a card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Create a free ScreenshotNeo account.
Best Value
- Used Book in Good Condition
Python and Node.js API examples
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Frequently Asked Questions
Can I define __dirname manually in an ESM Lambda file?
Yes. Use fileURLToPath(import.meta.url) and path.dirname, which derives a filesystem directory from the current module URL.
Does changing .mjs to .cjs fix Puppeteer?
It can fix the module error only if the package configuration and Lambda handler are changed consistently to CommonJS. It does not solve Chromium packaging or launch compatibility.
Is import.meta.dirname available in every Node 20 Lambda runtime?
No. It was introduced in Node 20.11, so verify the exact minor runtime configured for the function before relying on it.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →The Bottom Line
Use the URL-based ESM replacement unless you have verified import.meta.dirname support; switch to CommonJS only as a deliberate, whole-handler conversion. Then validate the independently packaged browser and Lambda deployment.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

