The Situation
It's 2 AM. Your backend service is throwing errors in production. You check the logs and see this:
TypeError: fetch is not a function
at callExternalAPI (/app/services/api.js:12:18)
at async processRequest (/app/handlers/request.js:34:5)
Your code worked fine locally. Your colleague's machine is fine too. And you just pushed a minor update. What happened?
Short answer: Node.js doesn't recognize fetch as a built-in โ because in your current Node version, it isn't.
Why This Happens
fetch started as a browser API. Node.js didn't ship a native implementation until Node.js 18 (released April 2022). Before that, calling fetch() without a polyfill would blow up with exactly this error.
Common triggers:
- Your production server runs Node.js 14 or 16, but your dev machine has Node.js 20
- You copied example code from a browser tutorial and dropped it into a Node.js project
- A Docker base image uses an older Node version than you expected
- You're on Node.js 17 and never enabled the
--experimental-fetchflag
Debug: Find the Root Cause First
Don't jump straight to a fix. Check which Node version is actually running in the environment where the error occurs:
node --version
Inside a Docker container:
docker exec -it your_container node --version
Below v18? That's your problem. Now pick the fix that fits your situation.
Fix 1: Upgrade to Node.js 18+ (Preferred)
If you control the runtime, this is the cleanest fix. No dependencies, no polyfills, no extra code.
# Using nvm
nvm install 18
nvm use 18
node --version # Should print v18.x.x or higher
For Docker, update your base image:
# Before
FROM node:16-alpine
# After
FROM node:18-alpine
Rebuild and redeploy. fetch is globally available โ no import needed.
Verify it works:
node -e "fetch('https://jsonplaceholder.typicode.com/todos/1').then(r => r.json()).then(console.log)"
You should see a JSON object printed to the terminal, not an error.
Fix 2: Use node-fetch (Stuck on an Older Node?)
Legacy project, vendor constraint, production freeze โ sometimes upgrading Node just isn't an option. Add node-fetch as a polyfill instead:
npm install node-fetch
Then in your code:
// CommonJS (require)
const fetch = require('node-fetch');
// ESM (import)
import fetch from 'node-fetch';
Watch out: node-fetch v3+ is ESM-only. If your project uses CommonJS (require()), pin to v2:
npm install node-fetch@2
The API is nearly identical to the browser's native fetch, so your existing code should work as-is.
Verify:
const fetch = require('node-fetch');
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(res => res.json())
.then(data => console.log(data));
Fix 3: Global Polyfill (Apply Once, Use Everywhere)
Rather than importing fetch in every single file, inject it globally at your app's entry point:
// At the very top of index.js or app.js
const fetch = require('node-fetch');
global.fetch = fetch;
// Now fetch works everywhere in the app without importing
This mimics browser behavior. Especially useful when dozens of files already call fetch and you don't want to touch each one.
Fix 4: Switch to axios (Skip the fetch Headache Entirely)
No strong attachment to fetch? axios works on every Node version from v10 upward, handles JSON automatically, and gives better error messages out of the box:
npm install axios
const axios = require('axios');
const response = await axios.get('https://api.example.com/data');
console.log(response.data);
The migration is simple: swap fetch(url).then(r => r.json()) for axios.get(url).then(r => r.data). That's usually the entire diff.
Fix 5: Node.js 17 โ Enable Experimental Fetch
Node 17 is a special case. fetch exists, but it's hidden behind a flag:
node --experimental-fetch your-script.js
To enable it permanently via package.json:
{
"scripts": {
"start": "node --experimental-fetch index.js"
}
}
Treat this as a temporary workaround, not a real fix. Node 17 hit end-of-life in June 2022 โ just upgrade to 18+.
The Mismatched Environment Problem
Here's the sneaky version of this bug: everything works on your laptop, then breaks the moment it hits CI or production. That's almost always a Node version mismatch between environments.
Lock your Node version explicitly. Add a .nvmrc file:
echo "18" > .nvmrc
Or set the engines field in package.json:
{
"engines": {
"node": ">=18.0.0"
}
}
For GitHub Actions, point setup-node at the file:
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
Now local, CI, and production all run the same version. No more midnight surprises.
Key Takeaways
- Pin your Node version. Use
.nvmrc, theenginesfield in package.json, and explicit Docker image tags โ nevernode:latest. - Browser APIs don't exist in Node by default.
fetch,localStorage,windowโ none of these are available unless you add them. - Watch your Docker base images.
node:alpinewith no version tag can quietly pull a different Node version depending on when the image was last pulled. - Keep environments in sync. If prod runs Node 16, your local dev should too โ not 20.

