Fix 'TypeError: fetch is not a function' in Node.js When Calling APIs

beginner๐Ÿ’š Node.js2026-05-03| Node.js < 18 (all OS: Linux, macOS, Windows); also Node.js 17 without --experimental-fetch flag

Error Message

TypeError: fetch is not a function
#fetch#http#node18#api#polyfill

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-fetch flag

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, the engines field in package.json, and explicit Docker image tags โ€” never node: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:alpine with 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.

Related Error Notes