Skip to content

Commit 03fc96c

Browse files
fix: route _log and _debug to stderr, simplify LINE regex, add troubleshooting docs
- Route _log() and _debug() output to console.error (stderr) instead of console.log (stdout). Diagnostic messages pollute stdout and break shell piping (e.g. node app.js | jq would include log output in the data stream). - Simplify the LINE regex: replace redundant (?:^|^) / (?:$|$) alternations with plain ^ / $ anchors. The original groups were logically identical to their simplified form and served no functional purpose. - Add a Troubleshooting section to README.md covering two very common user pain points: 1. Existing env vars not being overwritten (by design) and how to use { override: true } to bypass this. 2. .env file not loading and how to enable debug mode to diagnose it. - Update all test stubs in test-config.js, test-config-vault.js, and test-populate.js from sinon.stub(console, 'log') to sinon.stub(console, 'error') to match the new stderr output. All 194 tests pass.
1 parent 9d93f22 commit 03fc96c

File tree

5 files changed

+70
-28
lines changed

5 files changed

+70
-28
lines changed

README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,48 @@ Alternatively, just use [dotenv-webpack](https://github.com/mrsteele/dotenv-webp
576576

577577
 
578578

579+
## Troubleshooting
580+
581+
<details><summary>My environment variable is not being updated — it already exists in my shell</summary><br/>
582+
583+
By design, dotenv **never overwrites** a variable that already exists in `process.env`. This prevents accidental overrides of variables set by your CI/CD system, Docker environment, or shell profile.
584+
585+
**Why?** If you have `PORT=3000` set in your shell and `PORT=8080` in your `.env` file, your app will see `PORT=3000`. This matches The Twelve-Factor App recommendation that the environment always wins.
586+
587+
To deliberately override existing variables, use the `override` option:
588+
589+
```js
590+
require('dotenv').config({ override: true })
591+
```
592+
593+
Or with the preload flag:
594+
595+
```bash
596+
DOTENV_CONFIG_OVERRIDE=true node -r dotenv/config your_script.js
597+
```
598+
599+
> **Tip:** On Windows, system variables like `USERNAME`, `COMPUTERNAME`, and `PATH` are pre-set and will **not** be overwritten unless `override: true` is used.
600+
601+
</details>
602+
<details><summary>My `.env` file is not loading — variables are undefined</summary><br/>
603+
604+
Enable debug mode to get detailed output about what dotenv is doing:
605+
606+
```js
607+
require('dotenv').config({ debug: true })
608+
```
609+
610+
Common causes:
611+
- **Wrong working directory** — dotenv looks for `.env` relative to `process.cwd()`. Make sure you run your app from the project root.
612+
- **Typo in file name** — the file must be named `.env` (with a leading dot), not `env` or `.env.txt`.
613+
- **Incorrect path option** — if using a custom path, double-check it resolves correctly.
614+
615+
Use `{ path: '/absolute/path/to/.env' }` to specify an exact location.
616+
617+
</details>
618+
619+
&nbsp;
620+
579621
## Docs
580622

581623
Dotenv exposes four functions:

lib/main.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ function dim (text) {
4343
return supportsAnsi() ? `\x1b[2m${text}\x1b[0m` : text
4444
}
4545

46-
const LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
46+
const LINE = /^\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?$/mg
4747

4848
// Parse src into an Object
4949
function parse (src) {
@@ -132,11 +132,11 @@ function _warn (message) {
132132
}
133133

134134
function _debug (message) {
135-
console.log(`[dotenv@${version}][DEBUG] ${message}`)
135+
console.error(`[dotenv@${version}][DEBUG] ${message}`)
136136
}
137137

138138
function _log (message) {
139-
console.log(`[dotenv@${version}] ${message}`)
139+
console.error(`[dotenv@${version}] ${message}`)
140140
}
141141

142142
function _dotenvKey (options) {

tests/test-config-vault.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ t.afterEach(() => {
2828
t.test('does log when testPath calls to .env.vault directly (interpret what the user meant)', ct => {
2929
ct.plan(1)
3030

31-
logStub = sinon.stub(console, 'log')
31+
logStub = sinon.stub(console, 'error')
3232

3333
dotenv.config({ path: `${testPath}.vault` })
3434
ct.ok(logStub.called)
@@ -38,7 +38,7 @@ t.test('warns if DOTENV_KEY exists but .env.vault does not exist', ct => {
3838
ct.plan(1)
3939

4040
const testPath = 'tests/.env'
41-
logStub = sinon.stub(console, 'log')
41+
logStub = sinon.stub(console, 'error')
4242

4343
const existsSync = sinon.stub(fs, 'existsSync').returns(false) // make .env.vault not exist
4444
dotenv.config({ path: testPath })
@@ -52,7 +52,7 @@ t.test('warns if DOTENV_KEY exists but .env.vault does not exist (set as array)'
5252
ct.plan(1)
5353

5454
const testPath = 'tests/.env'
55-
logStub = sinon.stub(console, 'log')
55+
logStub = sinon.stub(console, 'error')
5656

5757
const existsSync = sinon.stub(fs, 'existsSync').returns(false) // make .env.vault not exist
5858
dotenv.config({ path: [testPath] })
@@ -65,7 +65,7 @@ t.test('warns if DOTENV_KEY exists but .env.vault does not exist (set as array)'
6565
t.test('logs when testPath calls to .env.vault directly (interpret what the user meant) and debug true', ct => {
6666
ct.plan(1)
6767

68-
logStub = sinon.stub(console, 'log')
68+
logStub = sinon.stub(console, 'error')
6969

7070
dotenv.config({ path: `${testPath}.vault`, debug: true })
7171
ct.ok(logStub.called)
@@ -260,7 +260,7 @@ t.test('can write to a different object rather than process.env', ct => {
260260

261261
process.env.ALPHA = 'other' // reset process.env
262262

263-
logStub = sinon.stub(console, 'log')
263+
logStub = sinon.stub(console, 'error')
264264

265265
const myObject = {}
266266

@@ -273,7 +273,7 @@ t.test('can write to a different object rather than process.env', ct => {
273273
t.test('logs when debug and override are turned on', ct => {
274274
ct.plan(1)
275275

276-
logStub = sinon.stub(console, 'log')
276+
logStub = sinon.stub(console, 'error')
277277

278278
dotenv.config({ path: testPath, override: true, debug: true })
279279

@@ -283,7 +283,7 @@ t.test('logs when debug and override are turned on', ct => {
283283
t.test('logs when debug is on and override is false', ct => {
284284
ct.plan(1)
285285

286-
logStub = sinon.stub(console, 'log')
286+
logStub = sinon.stub(console, 'error')
287287

288288
dotenv.config({ path: testPath, override: false, debug: true })
289289

tests/test-config.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ t.test('takes option for encoding', ct => {
120120
})
121121

122122
t.test('takes option for debug', ct => {
123-
logStub = sinon.stub(console, 'log')
123+
logStub = sinon.stub(console, 'error')
124124

125125
dotenv.config({ debug: 'true' })
126126
ct.ok(logStub.called)
@@ -231,7 +231,7 @@ t.test('returns any errors thrown from reading file or parsing', ct => {
231231
t.test('logs any errors thrown from reading file or parsing when in debug mode', ct => {
232232
ct.plan(2)
233233

234-
logStub = sinon.stub(console, 'log')
234+
logStub = sinon.stub(console, 'error')
235235
const readFileSyncStub = sinon.stub(fs, 'readFileSync').returns('test=foo')
236236

237237
readFileSyncStub.throws()
@@ -246,15 +246,15 @@ t.test('logs any errors thrown from reading file or parsing when in debug mode',
246246
t.test('logs any errors parsing when in debug and override mode', ct => {
247247
ct.plan(1)
248248

249-
logStub = sinon.stub(console, 'log')
249+
logStub = sinon.stub(console, 'error')
250250

251251
dotenv.config({ debug: true, override: true })
252252

253253
ct.ok(logStub.called)
254254
})
255255

256256
t.test('deals with file:// path', ct => {
257-
logStub = sinon.stub(console, 'log')
257+
logStub = sinon.stub(console, 'error')
258258

259259
const testPath = 'file:///tests/.env'
260260
const env = dotenv.config({ path: testPath })
@@ -269,7 +269,7 @@ t.test('deals with file:// path', ct => {
269269
})
270270

271271
t.test('deals with file:// path and debug true', ct => {
272-
logStub = sinon.stub(console, 'log')
272+
logStub = sinon.stub(console, 'error')
273273

274274
const testPath = 'file:///tests/.env'
275275
const env = dotenv.config({ path: testPath, debug: true })
@@ -284,7 +284,7 @@ t.test('deals with file:// path and debug true', ct => {
284284
})
285285

286286
t.test('path.relative fails somehow', ct => {
287-
logStub = sinon.stub(console, 'log')
287+
logStub = sinon.stub(console, 'error')
288288
const pathRelativeStub = sinon.stub(path, 'relative').throws(new Error('fail'))
289289

290290
const testPath = 'file:///tests/.env'
@@ -307,7 +307,7 @@ t.test('displays random tips from the tips array', ct => {
307307
const originalTTY = process.stdout.isTTY
308308
process.stdout.isTTY = true
309309

310-
logStub = sinon.stub(console, 'log')
310+
logStub = sinon.stub(console, 'error')
311311
const testPath = 'tests/.env'
312312

313313
// Test that tips are displayed (run config multiple times to see variation)
@@ -368,7 +368,7 @@ t.test('displays random tips from the tips array with fallback for isTTY false',
368368
const originalTTY = process.stdout.isTTY
369369
process.stdout.isTTY = undefined
370370

371-
logStub = sinon.stub(console, 'log')
371+
logStub = sinon.stub(console, 'error')
372372
const testPath = 'tests/.env'
373373

374374
// Test that tips are displayed (run config multiple times to see variation)
@@ -426,7 +426,7 @@ t.test('displays random tips from the tips array with fallback for isTTY false',
426426
t.test('logs when no path is set', ct => {
427427
ct.plan(1)
428428

429-
logStub = sinon.stub(console, 'log')
429+
logStub = sinon.stub(console, 'error')
430430

431431
dotenv.config()
432432
ct.ok(logStub.called)
@@ -436,7 +436,7 @@ t.test('does log by default', ct => {
436436
ct.plan(1)
437437

438438
const testPath = 'tests/.env'
439-
logStub = sinon.stub(console, 'log')
439+
logStub = sinon.stub(console, 'error')
440440

441441
dotenv.config({ path: testPath })
442442
ct.ok(logStub.called)
@@ -446,7 +446,7 @@ t.test('does not log if quiet flag passed true', ct => {
446446
ct.plan(1)
447447

448448
const testPath = 'tests/.env'
449-
logStub = sinon.stub(console, 'log')
449+
logStub = sinon.stub(console, 'error')
450450

451451
dotenv.config({ path: testPath, quiet: true })
452452
ct.ok(logStub.notCalled)
@@ -457,7 +457,7 @@ t.test('does not log if process.env.DOTENV_CONFIG_QUIET is true', ct => {
457457

458458
process.env.DOTENV_CONFIG_QUIET = 'true'
459459
const testPath = 'tests/.env'
460-
logStub = sinon.stub(console, 'log')
460+
logStub = sinon.stub(console, 'error')
461461

462462
dotenv.config({ path: testPath })
463463
ct.ok(logStub.notCalled)
@@ -468,7 +468,7 @@ t.test('does log if quiet flag false', ct => {
468468
ct.plan(1)
469469

470470
const testPath = 'tests/.env'
471-
logStub = sinon.stub(console, 'log')
471+
logStub = sinon.stub(console, 'error')
472472

473473
dotenv.config({ path: testPath, quiet: false })
474474
ct.ok(logStub.called)
@@ -479,7 +479,7 @@ t.test('does log if process.env.DOTENV_CONFIG_QUIET is false', ct => {
479479

480480
process.env.DOTENV_CONFIG_QUIET = 'false'
481481
const testPath = 'tests/.env'
482-
logStub = sinon.stub(console, 'log')
482+
logStub = sinon.stub(console, 'error')
483483

484484
dotenv.config({ path: testPath })
485485
ct.ok(logStub.called)
@@ -490,7 +490,7 @@ t.test('does log if quiet flag present and undefined/null', ct => {
490490
ct.plan(1)
491491

492492
const testPath = 'tests/.env'
493-
logStub = sinon.stub(console, 'log')
493+
logStub = sinon.stub(console, 'error')
494494

495495
dotenv.config({ path: testPath, quiet: undefined })
496496
ct.ok(logStub.called)
@@ -500,7 +500,7 @@ t.test('logs if debug set', ct => {
500500
ct.plan(1)
501501

502502
const testPath = 'tests/.env'
503-
logStub = sinon.stub(console, 'log')
503+
logStub = sinon.stub(console, 'error')
504504

505505
dotenv.config({ path: testPath, debug: true })
506506
ct.ok(logStub.called)

tests/test-populate.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ t.test('does write over keys already in processEnv if override turned on', ct =>
5959
t.test('logs any errors populating when in debug mode but override turned off', ct => {
6060
ct.plan(2)
6161

62-
const logStub = sinon.stub(console, 'log')
62+
const logStub = sinon.stub(console, 'error')
6363

6464
const parsed = { test: false }
6565
process.env.test = true
@@ -75,7 +75,7 @@ t.test('logs any errors populating when in debug mode but override turned off',
7575
t.test('logs populating when debug mode and override turned on', ct => {
7676
ct.plan(1)
7777

78-
const logStub = sinon.stub(console, 'log')
78+
const logStub = sinon.stub(console, 'error')
7979

8080
const parsed = { test: false }
8181
process.env.test = true

0 commit comments

Comments
 (0)