Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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).

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How Node decides the module type

  • .mjs files are ESM.
  • .cjs files are CommonJS.
  • A nearest package.json with "type":"module" treats .js as ESM; "type":"commonjs" treats them as CommonJS.
  • Ambiguous .js files 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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

  1. Confirm the handler identity. The setting must name the file and exported function you deployed. For an ESM index.mjs exporting handler, use index.handler. For a CommonJS index.cjs with exports.handler, use the same logical setting.
  2. 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.
  3. 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).
  4. Check layer layout. Node.js layers use nodejs/node_modules or a runtime-specific path such as nodejs/nodeXX/node_modules. Native and binary packages must be built for Linux and for the function’s architecture.
  5. 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.
  6. 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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
The SQL Programming Language: .
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.