I have debugged this class of problem more times than I care to remember, and every single time the experience is the same: two hours of staring at logs that look completely reasonable, with no error, no stack trace, nothing obviously wrong - just behavior that doesn't make sense until you realize the application was quietly connecting to the wrong database, sending emails to nowhere, or charging a live payment processor while running what you believed was a local test. The ancestor of all of these is the same: a hardcoded fallback for an environment variable that was never set.
So let me be direct, with some genuine frustration behind it: your application should refuse to start when a required environment variable is missing. Not fall back. Not log a warning and carry on. Stop. Scream. Die. Make it impossible to miss.
The Seductive Appeal of Fallbacks
It feels responsible to write this:
const DB_URL = process.env.DATABASE_URL ?? "postgres://localhost:5432/myapp";
It feels like good defensive work - thoughtful, considerate of the poor soul who clones the repo and forgets to copy .env.example. And for truly optional configuration like feature flags, log verbosity, or request timeouts, a fallback is perfectly reasonable. But for anything that determines where your application connects, who it talks to, and what it does with real data, a fallback is a landmine with a friendly face.
The problem is that fallbacks make wrong configurations look like right ones. Everything compiles, everything starts, requests flow through the system, and the only sign of trouble is buried in behavior rather than errors. By the time you notice, you may have sent a hundred welcome emails to /dev/null, written three hours of sessions to a SQLite file that gets wiped on every deploy, or done something irreversible to production data while running what you thought was a local test.
You Cannot Trust What You Cannot See
Silent fallbacks don't just hide configuration errors - they destroy your ability to reason about what the application is doing at any given moment. When a request behaves unexpectedly, your first question is always what configuration was this running under? If the honest answer is "the real value or the hardcoded default, depending on whether the variable was set, which we don't log and which changes between environments," you're not debugging anymore. You're doing archaeology.
Configuration should be explicit, visible, and verified at startup - not inferred at runtime from whatever happened to be set.
Failing Loudly Is an Act of Kindness
When your application crashes at startup with FATAL: DATABASE_URL is not set, you have given everyone involved an enormous gift. The developer sees it immediately, fixes it in thirty seconds, and moves on. The CI pipeline catches it before anything gets deployed. The on-call engineer, bleary-eyed at 2am, gets a clear signal rather than a mystery. Nobody has to infer that maybe the configuration was wrong - it says so, plainly, before any damage can be done.
Make your application know what it needs, check for it at the very first moment of startup, and refuse to proceed if anything required is absent, because that is the entire argument and everything else in this post is elaboration on why the alternatives are worse. Don't let it limp forward on assumptions and defaults, silently degrading into a state where nobody can tell whether it's working correctly or just appearing to.
Fail loudly, fail early, and fail before you've touched a single byte of real data.