<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lucas Ferreira</title>
    <description>The latest articles on DEV Community by Lucas Ferreira (@lksferreira).</description>
    <link>https://dev.to/lksferreira</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1673128%2F304a56e4-5c0d-4c65-9f89-9ec3287c900f.jpg</url>
      <title>DEV Community: Lucas Ferreira</title>
      <link>https://dev.to/lksferreira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lksferreira"/>
    <language>en</language>
    <item>
      <title>When Code "Works" by Accident: Hunting Down an Undocumented Fallback in node-pg-migrate 🔍</title>
      <dc:creator>Lucas Ferreira</dc:creator>
      <pubDate>Wed, 15 Jul 2026 21:01:30 +0000</pubDate>
      <link>https://dev.to/lksferreira/when-code-works-by-accident-hunting-down-an-undocumented-fallback-in-node-pg-migrate-4l7d</link>
      <guid>https://dev.to/lksferreira/when-code-works-by-accident-hunting-down-an-undocumented-fallback-in-node-pg-migrate-4l7d</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;We've all spent hours debugging a broken piece of code. But what happens when your code is supposed to fail, but it actually succeeds?&lt;/p&gt;

&lt;p&gt;Recently, while going through &lt;em&gt;Curso.dev&lt;/em&gt; (a popular web development course in Brazil), I decided to stray from the "happy path." I wanted to intentionally trigger a database connection error using the &lt;code&gt;node-pg-migrate&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;Specifically, I wanted to see how the tool would behave if I forgot to pass the &lt;code&gt;--envPath&lt;/code&gt; flag, which should have prevented it from accessing the &lt;code&gt;DATABASE_URL&lt;/code&gt; variable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My expectation:&lt;/strong&gt; Since &lt;code&gt;DATABASE_URL&lt;/code&gt; was empty, I expected a clear connection error (something like &lt;code&gt;client password must be a string&lt;/code&gt; or a generic connection failure).&lt;/p&gt;

&lt;p&gt;Instead, this happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;npm run migrate:up

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; clone-tabnews@1.0.0 migrate:up
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; node-pg-migrate &lt;span class="nt"&gt;--migrations-dir&lt;/span&gt; infra/migrations up

&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Migrating files:
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; - 1783567146909_inital
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; - 1783567172809_seconde
&lt;span class="c"&gt;### MIGRATION 1783567146909_inital (UP) ###&lt;/span&gt;
INSERT INTO &lt;span class="s2"&gt;"public"&lt;/span&gt;.&lt;span class="s2"&gt;"pgmigrations"&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;name, run_on&lt;span class="o"&gt;)&lt;/span&gt; VALUES &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1783567146909_inital'&lt;/span&gt;, NOW&lt;span class="o"&gt;())&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;### MIGRATION 1783567172809_seconde (UP) ###&lt;/span&gt;
INSERT INTO &lt;span class="s2"&gt;"public"&lt;/span&gt;.&lt;span class="s2"&gt;"pgmigrations"&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;name, run_on&lt;span class="o"&gt;)&lt;/span&gt; VALUES &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'1783567172809_seconde'&lt;/span&gt;, NOW&lt;span class="o"&gt;())&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

Migrations &lt;span class="nb"&gt;complete&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migrations ran and applied successfully. They connected to my cloud database on Neon without any &lt;code&gt;DATABASE_URL&lt;/code&gt; configured. 🤯&lt;/p&gt;

&lt;p&gt;Here is how I went down the &lt;code&gt;node_modules&lt;/code&gt; rabbit hole to figure out why.&lt;/p&gt;




&lt;h2&gt;
  
  
  Investigating the Unexpected
&lt;/h2&gt;

&lt;p&gt;One of the best lessons in software engineering is that we need to investigate not only when code breaks, but also when it behaves differently from what we expect.&lt;/p&gt;

&lt;p&gt;So, I started digging into the source code of the installed packages.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Version
&lt;/h3&gt;

&lt;p&gt;Inside &lt;code&gt;node_modules/node-pg-migrate/package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node-pg-migrate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"6.2.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"node-pg-migrate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bin/node-pg-migrate"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The &lt;code&gt;.env&lt;/code&gt; setup
&lt;/h3&gt;

&lt;p&gt;In my project's root &lt;code&gt;.env&lt;/code&gt; file, &lt;code&gt;DATABASE_URL&lt;/code&gt; was completely empty, but I had standard &lt;code&gt;PG*&lt;/code&gt; connection parameters (used by the default &lt;code&gt;pg&lt;/code&gt; module for other API endpoints):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL=

# Default libpq Connection Parameters
PGHOST='***-***-***-***-pooler.c-7.us-east-1.aws.neon.tech'
PGDATABASE='neondb'
PGUSER='neondb_owner'
PGPASSWORD='****************s'
PGSSLMODE='require'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Loading the environment variables
&lt;/h3&gt;

&lt;p&gt;Looking at &lt;code&gt;node_modules/node-pg-migrate/bin/node-pg-migrate&lt;/code&gt; (lines 217-239), I saw how the CLI loads environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* Load env before accessing process.env */&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dotenv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tryRequire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Load config from ".env" file&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;myEnv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dotenvConfig&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dotenvExpand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tryRequire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv-expand&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dotenvExpand&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;dotenvExpand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dotenvExpand&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;myEnv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;DB_CONNECTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;databaseUrlVarArg&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="c1"&gt;// process.env.DATABASE_URL&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;dotenv&lt;/code&gt; was a dependency in my project, the CLI automatically loaded the &lt;code&gt;.env&lt;/code&gt; file and populated &lt;code&gt;process.env&lt;/code&gt; with those &lt;code&gt;PG*&lt;/code&gt; variables.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The Undocumented Fallback
&lt;/h3&gt;

&lt;p&gt;Here is the culprit. Since &lt;code&gt;DATABASE_URL&lt;/code&gt; was empty, &lt;code&gt;DB_CONNECTION&lt;/code&gt; was evaluated as falsy, triggering the fallback block in the CLI (lines 380-387):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;DB_CONNECTION&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConnectionParameters&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;cp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`The $&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;databaseUrlVarArg&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;&lt;span class="s2"&gt; environment variable is not set.`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;DB_CONNECTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cp&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;new ConnectionParameters()&lt;/code&gt; class (which comes from the underlying &lt;code&gt;pg&lt;/code&gt; library) automatically looks for &lt;code&gt;PGHOST&lt;/code&gt;, &lt;code&gt;PGUSER&lt;/code&gt;, &lt;code&gt;PGPASSWORD&lt;/code&gt;, etc., in the environment when no connection string is provided.&lt;/p&gt;

&lt;p&gt;Because those variables were present in my &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;node-pg-migrate&lt;/code&gt; silently accepted them, bypassed the error, and successfully connected to the database anyway.&lt;/p&gt;




&lt;h2&gt;
  
  
  Documentation vs. Reality
&lt;/h2&gt;

&lt;p&gt;If you look at the &lt;a href="https://salsita.github.io/node-pg-migrate/getting-started" rel="noopener noreferrer"&gt;official node-pg-migrate documentation&lt;/a&gt;, it states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Now you should put your DB connection string to &lt;code&gt;DATABASE_URL&lt;/code&gt; environment variable and run &lt;code&gt;npm run migrate up&lt;/code&gt;."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The documentation implies that &lt;code&gt;DATABASE_URL&lt;/code&gt; is required. The fallback to standard &lt;code&gt;PG*&lt;/code&gt; environment variables is actually an undocumented implementation detail.&lt;/p&gt;

&lt;p&gt;While this works, relying on undocumented fallbacks is risky for a couple of reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fragility:&lt;/strong&gt; If &lt;code&gt;dotenv&lt;/code&gt; is missing or configured differently, this silent fallback fails instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configuration Drift:&lt;/strong&gt; You might assume your migrations are running locally, when they are actually applying to a remote database defined in your &lt;code&gt;PGHOST&lt;/code&gt; variables.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;They say "magic" in code is just logic we haven't read yet. When a piece of software does something unexpected, even if that "something" is succeeding when it should have failed, it's always worth digging in to understand why.&lt;/p&gt;

&lt;p&gt;This investigation made it clear exactly how &lt;code&gt;node-pg-migrate&lt;/code&gt; behaves under the hood:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It attempts to load &lt;code&gt;.env&lt;/code&gt; automatically using &lt;code&gt;tryRequire('dotenv')&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;It falls back to &lt;code&gt;ConnectionParameters()&lt;/code&gt; if &lt;code&gt;DATABASE_URL&lt;/code&gt; is missing.&lt;/li&gt;
&lt;li&gt;It uses standard PostgreSQL environment variables as a fallback option.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is also a great reminder of why comprehensive application monitoring and observability are so vital. When software fails, we usually get a loud error alert. But when software 'silently succeeds' through undocumented fallbacks, it bypasses traditional error catchers. Without proper environment auditing and logging, these silent configurations can drift into production completely unnoticed.&lt;/p&gt;

&lt;p&gt;To keep things predictable, I updated my setup to explicitly define &lt;code&gt;DATABASE_URL&lt;/code&gt; rather than relying on this silent fallback. Predictable code is always safer!&lt;/p&gt;

&lt;p&gt;Have you ever run into an undocumented fallback that saved (or almost broke) your setup? Let me know!&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>node</category>
      <category>database</category>
    </item>
  </channel>
</rss>
