The .env Surprise: Why Your dotenv Configuration Has No Effect in Bun

I am an enthusiastic researcher and developer with a passion for using technology to innovate in business and education.
Search for a command to run...

I am an enthusiastic researcher and developer with a passion for using technology to innovate in business and education.
No comments yet. Be the first to comment.
Ringkasan: Jika OpenClaw adalah AI Operating System yang menghubungkan berbagai layanan dan agent, maka Hermes adalah AI Worker yang fokus mengerjakan pekerjaan, belajar dari pengalaman, dan semakin p

Ketika membangun aplikasi modern seperti Next.js, Cloudflare Workers, platform AI, microservices, atau real-time dashboard, ada banyak istilah jaringan yang sering muncul: TCP, UDP, HTTP/2, QUIC, WebS

Setiap tahun, ribuan mahasiswa jurusan Teknologi Informasi, Sistem Informasi, dan Teknik Informatika diwisuda. Namun, tidak sedikit di antara mereka yang kemudian menghadapi kenyataan yang cukup berat

Jika Anda menggunakan Next.js, kemungkinan besar Anda pernah mendengar anggapan bahwa: "Next.js = Vercel" Padahal kenyataannya tidak harus demikian. Di sinilah OpenNext hadir. OpenNext adalah proyek

Saat pertama kali mencoba OpenCode Go, saya hampir langsung berlangganan tanpa membaca detail program yang mereka sediakan. Untungnya saya menemukan bahwa OpenCode memiliki program referral yang membe

For Node.js developers, the workflow is muscle memory: need environment variables? Install dotenv, call config(), and the variables from your .env file are ready to go. However, when you bring this habit to Bun, the newer, blazing-fast JavaScript runtime, you'll encounter a surprise—a bit of "magic" that renders your dotenv setup seemingly useless.
It's an anomaly that has stumped many developers: You have both a .env and a .env.local file. You explicitly call dotenv in your code, which should load .env. But when you run it, Bun loads the variables from .env.local instead.
This isn't a bug. It's a fundamental feature that showcases how Bun thinks differently.
Imagine the following simple project structure:
.env
DATABASE_URL="postgresql://default_user@db/common_database"
.env.local
DATABASE_URL="postgresql://local_user@db/my_special_database"
server.js
JavaScript
// The old habit from the Node.js world
import 'dotenv/config';
console.log(`Connecting to database using: ${process.env.DATABASE_URL}`);
Based on pure dotenv logic, the output you'd expect would come from the .env file, as dotenv.config() targets that file by default.
However, when you run bun run server.js, the output that appears in your console is:
Connecting to database using: postgresql://local_user@db/my_special_database
The value from .env.local wins. The dotenv package you imported appears to have been completely ignored. What's really going on?
The answer lies in the order of execution. Unlike Node.js, which is a "blank canvas," Bun is a "batteries-included" runtime. One of those "batteries" is a highly efficient, native environment variable loader.
Here is the actual sequence of events when you run bun run server.js:
Bun Takes Control (Before Your Code Runs): When the bun run command is executed, before a single line of your server.js is touched, the Bun runtime itself scans your project directory.
Native Loading by Bun: Bun automatically discovers all relevant .env files. It loads them in a predetermined order of precedence, where more specific files override more general ones. The rule is: .env.local always overrides .env.
process.env is Pre-populated: After this step, the global process.env object has already been populated by Bun. In our case, process.env.DATABASE_URL is already set to "postgresql://local_user@db/my_special_database".
Your Code Execution Begins: Only after all this preparation is complete does Bun start executing the code inside server.js.
The Futile dotenv Call: Your code reaches the import 'dotenv/config' line. The dotenv package runs and reads the .env file. However, when it's about to inject DATABASE_URL into process.env, it sees that the variable already exists. The default behavior of dotenv is not to overwrite existing variables. As a result, its call has no effect.
In short, Bun has already set the stage. The dotenv package you called arrived at a party that was already over.
Understanding this behavior is critical for working efficiently with Bun.
Ditch dotenv from Your Bun Projects: You simply don't need it. Relying on Bun's native loader is cleaner, faster, and removes one dependency from your project.
Trust Bun's Conventions: Leverage Bun's built-in .env precedence system. Use .env for team-wide defaults, .env.development or .env.production for environment-specific settings, and .env.local for your local overrides that should never be committed to Git.
A New Mindset: This is a perfect example of how Bun isn't just a "faster Node.js." It's an ecosystem with its own design philosophy that aims to simplify the modern development workflow.
So, the next time you see "weird" behavior in Bun, remember that it's often not a bug, but a clever feature designed to make your life easier—even if it means unlearning a few old habits.