Greetings.
CommonJS require()
In CommonJS, importing a JSON file is straightforward:
Node.js parses the file automatically, returning a plain JavaScript object. No additional steps needed.
Switching to ES Modules
Once "type": "module" is set in package.json, require() is no longer available. Here are the alternatives.
-
readFileSyncfromfsSynchronous. It blocks execution until the file is fully read:
Or add
path.resolve():Or add
dirnameif the process is launched from an arbitrary directory:💡
path.resolve()without__dirnameresolves relative to the current working directory. Not strictly needed, depends on the context.💡 Without
path.resolve(), the relative path string passed directly intoreadFileSyncis still resolved against the current working directory . But with it, the path resolution becomes explicit.As you can see above, there's difference, path starts with
./config/and justconfig/. -
readFilefromfs/promisesAsynchronous, non-blocking. Using top-level
awaitavailable in ES modules (Node 14.8+):With
path.resolve():With
dirname:
⬆️ The Timing of Both
readFileSync and readFile load the entire file into memory before returning the data — neither streams it in chunks (documentation on Node.js). The difference lies in how each interacts with the Node.js event loop.
readFileSync halts execution on the main thread. Nothing else runs until the file has been read completely. This is rather similar to what require() was doing in CommonJS. For loading a small config file once at startup, this is acceptable — the blocking is brief and inconsequential.
readFile with await hands the I/O operation off to the underlying system and yields control back to the event loop while waiting. Other work can proceed in the meantime.
However, if we are building a server with Node.js, using readFileSync inside a request handler can make the server slower, as each request may get blocked while waiting for the file to be read (article on GeeksforGeeks).
There is something worth noting on raw performance. For large files, readFileSync can be measurably faster than readFile due to avoiding excessive context switching (on GitHub issue). For a small JSON config file, this difference is negligible. For reading many small files in sequence, synchronous code can outperform asynchronous code significantly (article on Medium), though that scenario rarely applies to a single config read at startup.
In short, the right choice depends on context rather than an absolute rule:
readFileSync |
readFile + await |
|
|---|---|---|
| Execution | Blocks the event loop | Non-blocking |
| Top-level usage | Always | ES modules only (Node 14.8+) |
| JSON parsing | Manual. Using JSON.parse |
Manual. Using JSON.parse |
| Raw speed (large files) | Faster | Slightly slower |
| Server request handler | Inadvisable | Preferred |
The assert and with Keywords
We may encounter this syntax:
It is because ESLint uses Espree as its default parser. And ESLint's parser only officially supports the latest final ECMAScript standard — accepting new syntax only once a proposal reaches Stage 4 in the TC39 process. The assert keyword came from the Import Assertions proposal (GitHub link), which was at Stage 3 when Node.js began shipping it experimentally. ESLint deliberately declined to support it at that stage, as the team had no guarantee of which ECMAScript version it would ultimately land in (discussion on GitHub). The proposal was subsequently withdrawn entirely, never reaching Stage 4.
Its replacement is the Import Attributes proposal, using with instead:
But we may still come across similar warning from ESLint. Even on Node.js v22 with ESLint v9, Espree throws Parsing error: Unexpected token 'with' — the same root cause.
The good news is that Import Attributes did reach Stage 4 and was incorporated into ES2025. ESLint tracked this and accepted the issue, with support for the with syntax merged into Espree and ESLint's core rules accordingly (on GitHub issue). Support is available from ESLint v9.18.0 onwards, with ecmaVersion set to 2025 or "latest" in the ESLint configuration:
For projects on older ESLint versions, @babel/eslint-parser can be used as a custom parser to handle experimental syntax that Espree does not yet support (ESLint documentation on configuring a parser).
That said, for any Node.js version below 20, or where tooling compatibility is uncertain, the fs-based approaches remain the most reliable option without additional configuration.
I'm on latest Node.js and most of my projects are in ES modules. Currently, I use the fs method. Specifically, the readFile + await.
Comments
Post a Comment