Tình huống
2 giờ sáng. Service backend của bạn đang báo lỗi trên production. Bạn kiểm tra log và thấy dòng này:
TypeError: fetch is not a function
at callExternalAPI (/app/services/api.js:12:18)
at async processRequest (/app/handlers/request.js:34:5)
Code chạy ngon trên máy local. Máy đồng nghiệp cũng không có vấn đề gì. Và bạn vừa push một bản cập nhật nhỏ. Chuyện gì đã xảy ra?
Câu trả lời ngắn gọn: Node.js không nhận fetch là hàm có sẵn — vì với phiên bản Node bạn đang dùng, nó thực sự không có.
Nguyên nhân
fetch ban đầu là API của trình duyệt. Node.js không tích hợp sẵn cho đến Node.js 18 (phát hành tháng 4/2022). Trước đó, gọi fetch() mà không có polyfill sẽ gây ra đúng lỗi này.
Các nguyên nhân thường gặp:
- Server production đang chạy Node.js 14 hoặc 16, trong khi máy dev của bạn dùng Node.js 20
- Bạn copy code ví dụ từ tutorial dành cho trình duyệt rồi đưa vào project Node.js
- Docker base image dùng phiên bản Node cũ hơn bạn nghĩ
- Bạn đang dùng Node.js 17 và chưa bật flag
--experimental-fetch
Debug: Tìm nguyên nhân gốc rễ trước
Đừng vội vào fix ngay. Hãy kiểm tra phiên bản Node thực sự đang chạy trong môi trường xảy ra lỗi:
node --version
Bên trong Docker container:
docker exec -it your_container node --version
Thấp hơn v18? Đó là vấn đề của bạn. Giờ hãy chọn cách fix phù hợp với tình huống.
Fix 1: Nâng cấp lên Node.js 18+ (Khuyến nghị)
Nếu bạn kiểm soát được runtime, đây là cách fix sạch nhất. Không cần thêm dependency, không cần polyfill, không cần thêm code.
# Dùng nvm
nvm install 18
nvm use 18
node --version # Sẽ in ra v18.x.x hoặc cao hơn
Với Docker, cập nhật base image:
# Trước
FROM node:16-alpine
# Sau
FROM node:18-alpine
Rebuild và redeploy. fetch đã có sẵn toàn cục — không cần import.
Kiểm tra lại:
node -e "fetch('https://jsonplaceholder.typicode.com/todos/1').then(r => r.json()).then(console.log)"
Bạn sẽ thấy một JSON object được in ra terminal, không phải lỗi.
Fix 2: Dùng node-fetch (Khi không thể nâng cấp Node?)
Project legacy, ràng buộc từ vendor, production bị đóng băng — đôi khi nâng cấp Node không phải lựa chọn khả thi. Hãy thêm node-fetch như một polyfill:
npm install node-fetch
Rồi trong code của bạn:
// CommonJS (require)
const fetch = require('node-fetch');
// ESM (import)
import fetch from 'node-fetch';
Lưu ý: node-fetch v3+ chỉ hỗ trợ ESM. Nếu project dùng CommonJS (require()), hãy dùng v2:
npm install node-fetch@2
API gần như giống hệt fetch của trình duyệt, nên code hiện tại của bạn sẽ chạy được ngay.
Kiểm tra:
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 (Khai báo một lần, dùng được khắp nơi)
Thay vì import fetch trong từng file, hãy inject nó vào global ngay tại entry point của ứng dụng:
// Đặt ở đầu file index.js hoặc app.js
const fetch = require('node-fetch');
global.fetch = fetch;
// Giờ fetch hoạt động ở mọi nơi trong app mà không cần import
Cách này mô phỏng hành vi của trình duyệt. Đặc biệt hữu ích khi hàng chục file đã gọi fetch và bạn không muốn sửa từng file một.
Fix 4: Chuyển sang axios (Tránh hoàn toàn rắc rối với fetch)
Không nhất thiết phải dùng fetch? axios hoạt động trên mọi phiên bản Node từ v10 trở lên, tự động xử lý JSON, và cho thông báo lỗi rõ ràng hơn:
npm install axios
const axios = require('axios');
const response = await axios.get('https://api.example.com/data');
console.log(response.data);
Việc chuyển đổi rất đơn giản: thay fetch(url).then(r => r.json()) bằng axios.get(url).then(r => r.data). Thường chỉ có vậy thôi.
Fix 5: Node.js 17 — Bật Experimental Fetch
Node 17 là trường hợp đặc biệt. fetch có tồn tại, nhưng bị ẩn sau một flag:
node --experimental-fetch your-script.js
Để bật vĩnh viễn qua package.json:
{
"scripts": {
"start": "node --experimental-fetch index.js"
}
}
Hãy coi đây là giải pháp tạm thời, không phải fix thực sự. Node 17 đã hết vòng đời vào tháng 6/2022 — cứ nâng lên 18+ đi.
Vấn đề môi trường không đồng nhất
Đây là dạng bug tinh vi: mọi thứ chạy tốt trên laptop của bạn, rồi lại hỏng ngay khi lên CI hoặc production. Gần như chắc chắn đó là do phiên bản Node khác nhau giữa các môi trường.
Hãy khóa phiên bản Node một cách rõ ràng. Thêm file .nvmrc:
echo "18" > .nvmrc
Hoặc khai báo trường engines trong package.json:
{
"engines": {
"node": ">=18.0.0"
}
}
Với GitHub Actions, trỏ setup-node vào file đó:
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
Giờ local, CI và production đều chạy cùng phiên bản. Không còn bị "đánh thức" lúc nửa đêm nữa.
Tóm lại
- Khóa phiên bản Node của bạn. Dùng
.nvmrc, trườngenginestrong package.json, và chỉ định rõ tag cho Docker image — đừng bao giờ dùngnode:latest. - API của trình duyệt không có sẵn trong Node.
fetch,localStorage,window— không cái nào tồn tại trong Node trừ khi bạn tự thêm vào. - Chú ý Docker base image.
node:alpinekhông có tag phiên bản có thể âm thầm pull một phiên bản Node khác tùy thời điểm image được pull lần cuối. - Giữ các môi trường đồng nhất. Nếu production chạy Node 16, local dev của bạn cũng nên vậy — không phải 20.

