dotenv icon indicating copy to clipboard operation
dotenv copied to clipboard

How do we specify `quiet: true` when importing `dotenv` when using ES6?

Open dossy opened this issue 6 months ago • 10 comments

Following the change in v17 (#874), what is the proper way to specify quiet: true when importing dotenv when using ES6?

Per current usage documentation:

import 'dotenv/config'

How do we specify configuration options like quiet: true?

Creating a separate file just for importing dotenv in order to specify config options is not a satisfying developer experience.

See also: #89, #133

dossy avatar Jul 01 '25 15:07 dossy

Currently I haven't changed import 'dotenv/config'. It defaults to quiet: true still. Because, yes, I agree that wrapping a separate file to add a simple config change here for ES6 is an absolutely awful developer experience.

That said, it IS sometimes useful to change your config options - maybe you want debug in your tests. In which case, I recommend @dotenvx/dotenvx. It includes a cli so then you can just do this:

{
  "scripts": {
    "start": "dotenvx run --quiet -- node index.js",
    "test": "dotenvx run --debug -- node tests.js",
  },
  "dependencies": {
    "@dotenvx/dotenvx": "^1.45.1"
  }
}

Which I think is a great developer experience. We've solved a lot of edge cases in dotenvx - even the coverage is better - https://dotenvx.com/spec.

motdotla avatar Jul 01 '25 16:07 motdotla

Also, thank you for taking the time to link references to all those issues in your ticket. Very helpful.

motdotla avatar Jul 01 '25 16:07 motdotla

import dotenv from 'dotenv';

dotenv.config({ quiet: true });

micobarac avatar Sep 03 '25 06:09 micobarac

import dotenv from 'dotenv';

dotenv.config({ quiet: true });

@micobarac, have you tried it?

Unless things have changed so that #133 is no longer true, I don't think it does what you think it does.

dossy avatar Sep 03 '25 06:09 dossy

@dossy Yes, I tried with "dotenv": "^17.2.1". Let me share those outputs:

dotenv.config();
~/workspace/demo (next)$ yarn generate
yarn run v1.22.22
$ npx tsc --project tsconfig.json && ts-node dist/generate.js
[[email protected]] injecting env (5) from .env -- tip: ⚙️  suppress all logs with { quiet: true }
Documentation generated successfully!
✨  Done in 2.15s.
dotenv.config({ quiet: true });
~/workspace/demo (next)$ yarn generate
yarn run v1.22.22
$ npx tsc --project tsconfig.json && ts-node dist/generate.js
Documentation generated successfully!
✨  Done in 2.21s.

micobarac avatar Sep 03 '25 06:09 micobarac

Is dist/generate.js also importing other scripts that are trying to access values loaded by dotenv into process.env successfully?

dossy avatar Sep 03 '25 06:09 dossy

I actually use generate.ts, which imports libraries and uses dotenv and gets transpiled into generate.js:

import cassandra from 'cassandra-driver';
import dotenv from 'dotenv';
import fs from 'fs';
import path from 'path';
import prettier from 'prettier';
import { IndentationText, NewLineKind, Project, PropertyAssignment, SyntaxKind } from 'ts-morph';

// --- Environment Variables ---
dotenv.config({ quiet: true });

const requiredEnv = ['CASSANDRA_HOST', 'CASSANDRA_PORT', 'CASSANDRA_USER', 'CASSANDRA_PASSWORD', 'CASSANDRA_KEYSPACE'];

for (const key of requiredEnv) {
  if (!process.env[key]) throw new Error(`Missing environment variable: ${key}`);
}

const DB_HOST = process.env.CASSANDRA_HOST!;
const DB_PORT = parseInt(process.env.CASSANDRA_PORT!, 10);
const DB_USERNAME = process.env.CASSANDRA_USER!;
const DB_PASSWORD = process.env.CASSANDRA_PASSWORD!;
const KEYSPACE = process.env.CASSANDRA_KEYSPACE!;

micobarac avatar Sep 03 '25 07:09 micobarac

Right, you're accessing all of your process.env variables from your top-level script where you're import'ing dotenv.

That works just fine.

Try:

$ yarn init -y
yarn init v1.22.22
warning The yes flag has been set. This will automatically answer yes to all questions, which may have security implications.
success Saved package.json
✨  Done in 0.02s.

$ yarn add dotenv
yarn add v1.22.22
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
success Saved 1 new dependency.
info Direct dependencies
└─ [email protected]
info All dependencies
└─ [email protected]
✨  Done in 0.36s.

$ node -e 'fs = require("fs"); json = JSON.parse(fs.readFileSync("package.json")); json.type = "module"; fs.writeFileSync("package.json", JSON.stringify(json));'

$ echo 'FOO=bar' >.env

$ cat <<-EOT >index.js
import dotenv from 'dotenv';
dotenv.config();
import './foo.js';

console.log('index.js FOO=', process.env.FOO);
EOT

$ cat <<-EOT >quiet.js
import dotenv from 'dotenv';
dotenv.config({ quiet: true });
import './foo.js';

console.log('quiet.js FOO=', process.env.FOO);
EOT

$ cat <<-EOT >foo.js
console.log('foo.js FOO=', process.env.FOO);
EOT

$ node index.js
foo.js FOO= undefined
[[email protected]] injecting env (1) from .env -- tip: 🔐 encrypt with Dotenvx: https://dotenvx.com
index.js FOO= bar

$ node quiet.js
foo.js FOO= undefined
quiet.js FOO= bar

While the dotenv.config({ quiet: true }) does set the setting as expected, because import declarations are hoisted in ES modules, the requisite dotenv.config() call occurs after all of the imports.

The current guidance to deal with this is to use import 'dotenv/config' but that mechanism does not provide a way to pass any configuration options, such as quiet: true.

Personally, I'm a huge fan of keeping all code in a single file, but so many devs out there insist on scattering their code across many files, and for them, there's currently no good way of importing dotenv as an ES module and setting config options.

The current workaround from 2016 is to move your import of dotenv into its own file which then gets imported from the top-level script, but that's not a very satisfying developer experience in my opinion.

I opened this issue in hopes that someone comes up with a clean solution to this problem, although I suspect due to the nature of how ES modules hoist imports, the only option is the workaround described above.

dossy avatar Sep 03 '25 14:09 dossy

Hi, thanks @dossy for the detailed issue. I've done some testing based on this discussion and wanted to post a summary for anyone else encountering this problem.

  1. The Core Problem is ESM Hoisting

I can confirm the root cause is ESM hoisting.

import dotenv from 'dotenv'; 
dotenv.config(); 

pattern fails because the JavaScript engine processes other import statements (like import './foo.js') before the dotenv.config() line is executed.

On newer Node.js versions (e.g., v18+), it seems the module loader is improved enough that

import 'dotenv/config'

at the top of the entry file works correctly.

  1. Better Solution: The --require Flag

For a solution that is explicit, requires no extra dependencies, and works reliably across all Node.js versions, the best approach is to use Node.js's native --require (or -r) flag.

This method handles environment variable loading before your application code starts, making any in-code hoisting issues irrelevant.

How to Implement It: Simply add the flag to your package.json scripts. When using this method, you should remove any dotenv import statements from your JavaScript files.

// package.json
"scripts": {
  "start": "node --require dotenv/config index.js"
}

This approach clearly separates the environment setup from the application logic and I think it is a better way to solve the problem without adding external dependencies like @dotenvx/dotenvx.

Hope this summary helps clarify the situation for others!

uthem150 avatar Sep 20 '25 08:09 uthem150

  1. Better Solution: The --require Flag

This doesn't solve the "how do I pass configuration options to dotenv.config()" problem, though, and introduces unnecessary complexity, requiring every executor of the code to know to use --require.

This also assumes that everyone is using the Node.js runtime. What about folks who are using Deno or Bun, just to name two popular (as of 2025) alternatives?

dossy avatar Sep 26 '25 23:09 dossy