ESM vs CommonJS on Falix

Two module systems, one Node runner. What "type":"module" actually flips, which errors still bite on Falix's Node, and the exact fix for each — verified in the real container.

Node has two ways to split code into files: CommonJS (require / module.exports), the original, and ES Modules (import / export), the newer standard. Most "why won't my code run" confusion on the Node.js application comes from mixing the two by accident. This guide shows how Node decides which mode a file is in, exactly what breaks when you get it wrong, and the one-line fix for each — all on the same runner Falix uses.

At a glance
You need A Falix server running the Node.js application
Plan Any — the module system is the same on free and premium
Time Fifteen minutes
Good to already know Node.js on Falix (entry file, auto npm install)

The two systems at a glance

CommonJS (CJS) ES Modules (ESM)
Import const x = require('x') import x from 'x'
Export module.exports = ... export / export default
Folder path __dirname, __filename import.meta.dirname, import.meta.url
Top-level await No Yes
Loads the other kind import() (dynamic) or require() an ESM import works for both

Neither is "better" for a Falix server — both run fine. What matters is that every file is in exactly one mode, and that you don't reach for the wrong keyword inside it.

How Node decides a file's mode

The Node.js application starts your code with node <Main file> (the Main file variable, default index.js). Before running a line, Node picks the file's mode by these rules, in order:

  1. The extension wins if it's explicit. A .mjs file is always ESM. A .cjs file is always CommonJS. Nothing overrides these.
  2. For a .js file, the nearest package.json decides. If it contains "type": "module", every .js file in that folder tree is ESM. If it says "type": "commonjs" or has no type field, .js is CommonJS.
  3. Modern Node also sniffs the syntax. On the Node version Falix currently defaults to, a .js file with no type set that clearly uses import/export is detected and run as ESM automatically — so a lone import at the top of index.js may just work with no config at all.

🎯 Good to know: That last rule is why old tutorials warning "you must set type:module before you can use import" are now half-true. Detection is convenient, but it's fragile — the moment a file mixes paradigms, or you pick an older Node version in Settings, the strict errors below come back. Choosing one system on purpose beats relying on the sniffer.

What actually breaks — and the fix

These are the errors you'll really see in the Console, each with why it happens and the one change that fixes it:

Console error What you did Fix
ReferenceError: require is not defined in ES module scope Called require() inside an ESM file ("type":"module" or .mjs) Switch that line to import, or rename the file to .cjs
ReferenceError: __dirname is not defined in ES module scope Used __dirname/__filename in ESM Use import.meta.dirname / import.meta.filename
ReferenceError: module is not defined in ES module scope Used module.exports in ESM Use export / export default instead
SyntaxError: Cannot use import statement outside a module Used import in a file Node treats as CommonJS — a .cjs file, or any .js on an older Node version Rename to .mjs, set "type":"module", or use require()
Error [ERR_REQUIRE_ASYNC_MODULE] require()d an ESM file that uses top-level await Load it with dynamic import() and await the result

Notice what is not on that list: on Falix's current Node runner, require()-ing an ordinary ESM module works — the old ERR_REQUIRE_ESM crash is largely gone. The one case that still stops require() is an ESM module that uses top-level await; there you must switch to dynamic import().

The "type": "module" flip, concretely

Adding one line to package.json re-labels every .js file in the project as ESM:

{
  "type": "module"
}

After that flip, three things you may have written as CommonJS stop existing and need their ESM equivalents:

// Before (CommonJS) — now throws "require/module/__dirname is not defined"
const fs = require('node:fs');
module.exports = { start };
const here = __dirname;

// After (ESM) — the equivalents
import fs from 'node:fs';
export { start };
const here = import.meta.dirname;   // the folder this file lives in

If you'd rather keep one stubborn file as CommonJS inside an otherwise-ESM project, rename just that file to .cjs — the extension overrides the type field. The reverse works too: a single .mjs file stays ESM even when package.json has no type.

Loading one system from the other

You'll sometimes need a library that only ships as ESM from CommonJS code (or vice-versa). The bridge is dynamic import(), which returns a promise and works from both systems:

// CommonJS file loading an ESM-only dependency
async function main() {
  const { default: fetch } = await import('node-fetch');
  const res = await fetch('https://example.com');
  console.log(res.status);
}
main();

This is also the fix for ERR_REQUIRE_ASYNC_MODULE: whenever require() refuses because the target uses top-level await, await import(...) instead.

⚠️ Heads up: Don't scatter both require and import through one file hoping Node sorts it out. Pick the file's system, keep every static import in that style, and reach for dynamic import() only as the deliberate bridge between the two.

Which should you pick?

  • Following a tutorial or an existing codebase? Match whatever it uses. Most bot and Express tutorials are still CommonJS (require); many newer libraries and guides are ESM (import).
  • Starting fresh? ESM is the standard the ecosystem is moving to, and it's what you'll write everywhere else. Set "type": "module" once and use import throughout.
  • Want TypeScript with no build step and no module-mode juggling at all? The Bun application runs both systems and TypeScript natively.

Troubleshooting

  • "It ran on my laptop but crashes on Falix" — you may be on a different Node version. The Settings page runtime dropdown lets you match your local major (Node 12–25); older majors are stricter about the errors above. See Node version errors.
  • A dependency's README says "ESM only" — you don't have to convert your whole project. Load that one package with await import(...) from your existing CommonJS code.
  • Edits to a .ts file do nothing / won't run — that's a different topic: a bare .ts Main file doesn't run on the Node app. See TypeScript projects on Falix.
  • Cannot find module after switching styles — an unrelated missing dependency; read the npm output at the top of the console and install it from the Packages page.

Everything past this is standard Node behavior — the official reference at nodejs.org covers the full module-resolution rules if you want the deep version.


Next steps

Was this guide helpful?