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:
- The extension wins if it's explicit. A
.mjsfile is always ESM. A.cjsfile is always CommonJS. Nothing overrides these. - For a
.jsfile, the nearestpackage.jsondecides. If it contains"type": "module", every.jsfile in that folder tree is ESM. If it says"type": "commonjs"or has notypefield,.jsis CommonJS. - Modern Node also sniffs the syntax. On the Node version Falix currently defaults to, a
.jsfile with notypeset that clearly usesimport/exportis detected and run as ESM automatically — so a loneimportat the top ofindex.jsmay just work with no config at all.
🎯 Good to know: That last rule is why old tutorials warning "you must set
type:modulebefore you can useimport" 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
requireandimportthrough one file hoping Node sorts it out. Pick the file's system, keep every static import in that style, and reach for dynamicimport()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 useimportthroughout. - 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
.tsfile do nothing / won't run — that's a different topic: a bare.tsMain file doesn't run on the Node app. See TypeScript projects on Falix. Cannot find moduleafter 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.