Apa yang sebenarnya dikeluhkan error ini

Ketika Anda sedang membangun aplikasi dengan Node.js, terutama pada versi-versi terbaru yang secara default menggunakan ECMAScript Modules (ESM), momen frustrasi hampir selalu dimulai dengan layar terminal yang memuntahkan pesan error berwarna merah. Pesan tersebut biasanya terlihat persis seperti berikut:

node:internal/modules/cjs/loader:936
  throw err;
  ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/path/ke/proyek-anda/fileYangDicari.js'
    at new NodeError (node:internal/errors:372:5)
    at finalizeResolution (node:internal/modules/cjs/loader:433:5)
    at resolve (node:internal/modules/cjs/loader:312:12)
    at file:///path/ke/proyek-anda/index.js:3:1 {
  code: 'ERR_MODULE_NOT_FOUND',
  url: 'file:///path/ke/proyek-anda/fileYangDicari.js'
}

Jika kita bedah kata per kata, keluhan utama dari error ini sangatlah eksplisit: Node.js mencoba untuk memuat sebuah berkas modul dari alamat (path) tertentu, tetapi modul tersebut tidak ada di sana. Dalam konteks ekosistem Node.js modern, error ERR_MODULE_NOT_FOUND secara spesifik merujuk pada kegagalan sistem resolver modul untuk menemukan berkas yang spesifikasinya ditulis menggunakan sintaks import (ESM), atau ketika ekstensi berkas diabaikan padahal sistem mengharuskannya.

Dalam bahasa Indonesia sederhana, error ini berarti: "Node.js tidak bisa menemukan berkas modul yang Anda minta untuk di-impor. Coba periksa kembali alamat atau nama berkas yang Anda tulis di pernyataan import."

Asal masalahnya Secara fundamental, ini terjadi karena ada diskrepansi antara apa yang ditulis oleh programer di dalam kode sumber dan kondisi faktual di dalam sistem berkas (file system). Sistem modul di Node.js, terutama ketika berjalan di bawah regime ESM (yang diaktifkan melalui "type": "module" di package.json atau ekstensi .mjs), menerapkan aturan resolusi yang sangat ketat. Berbeda dengan CommonJS (CJS) yang sangat permisif dan mencoba menebak ekstensi berkas (misalnya jika Anda menulis require('./utils'), Node.js akan mencari utils.js, utils/index.js, atau utils.node), sistem ESM mengharuskan Anda menulis alamat modul secara eksplisit dan lengkap, termasuk ekstensi berkasnya.

Sebagai contoh, perhatikan potongan kode berikut:

// kode di dalam index.js
import { helperFunction } from './helpers';

Jika Anda menjalankan kode di atas di lingkungan ESM, Node.js akan mencari berkas atau folder bernama tepat helpers tanpa ekstensi. Karena berkas Anda mungkin bernama helpers.js, pencarian ini akan gagal, dan error ERR_MODULE_NOT_FOUND akan dilempar. Ini adalah perbedaan perilaku yang paling sering menyebabkan kebingungan bagi developer yang baru bermigrasi dari CommonJS ke ESM, karena mereka beranggapan Node.js akan "pintar" enough untuk menebak ekstensi .js, padahal dalam mode ESM, tebakan seperti itu sengaja tidak diimplementasikan demi determinisme dan performa resolver.

Kenapa ini terjadi

Meskipun pesan errornya terlihat sederhana, akar masalah yang merujuk pada ERR_MODULE_NOT_FOUND bisa sangat beragam, mulai dari kesalahan ketik yang sepele hingga konflik konfigurasi yang rumit. Berikut adalah 5 root cause paling sering terjadi, diurutkan berdasarkan frekuensi kemunculannya di dunia nyata:

  1. Ekstensi berkas diabaikan pada impor ESM: Ini adalah penyebab nomor satu. Sistem ESM mengharuskan Anda menulis import something from './file.js', bukan import something from './file'. Menghilangkan .js akan membuat resolver mencari entri tepat sesuai string literal, yang bukan merupakan berkas.
  2. Kesalahan penulisan jalur (typo) atau case-sensitivity: Anda menulis import data from './UserDaTa', sementara berkas aslinya bernama userData.js. Sistem berkas di macOS dan Windows secara historis bersifat case-insensitive (mereka akan menemukan berkas tersebut meskipun kapitalisasi salah), namun ketika kode di-deploy ke server Linux (yang case-sensitive), impor akan tiba-tiba gagal.
  3. Berkas atau direktori memang tidak ada: Anda mengimpor modul yang belum dibuat, atau Anda baru saja melakukan refactor dengan memindahkan berkas ke folder lain lupa memperbarui jalur impornya.
  4. Kegagalan resolusi bare specifier (dependensi pihak ketiga): Anda menulis import lodash from 'lodash', namun paket tersebut tidak terinstal di direktori node_modules, atau berkas entry point yang dideklarasikan di package.json paket tersebut tidak ada di dalam paket tersebut.
  5. Konflik antara CJS dan ESM: Anda memaksa berkas .js berjalan sebagai ESM melalui "type": "module", tetapi di dalam kode Anda menggunakan sintaks require(), atau sebaliknya, Anda mencoba mengimpor paket CJS tanpa wrapper yang tepat sehingga resolver gagal menemukan ekspor yang diharapkan.

Cara mempersempit kemungkinan penyebab:

Langkah diagnosis pertama adalah melihat string jalur yang gagal di dalam pesan error. Node.js selalu mencetak properti url atau path yang menyebabkan kegagalan. Perhatikan baik-baik apakah jalur tersebut diakhiri dengan ekstensi berkas atau tidak. Jika tidak, penyebabnya adalah nomor 1.

Selanjutnya, jika jalur sudah benar dan disertai ekstensi, buka terminal dan jalankan perintah ls atau dir pada direktori kerja Anda untuk memverifikasi keberadaan berkas tersebut. Bandingkan karakter per karakter antara nama berkas di terminal dengan nama berkas di kode. Jika terdapat perbedaan kapitalisasi huruf, penyebabnya adalah nomor 2.

Jika berkas memang tidak ada, penyebabnya adalah nomor 3. Namun, jika jalur yang diimpor diawali dengan ./ atau ../ dan berkas ada, namun masih error, periksa apakah Anda menjalankan Node.js dengan flag eksperimental yang memengaruhi resolusi. Sebaliknya, jika jalur yang diimpor tidak diawali dengan titik (misalnya import 'some-package'), ini adalah bare specifier. Jalankan npm ls some-package atau pnpm ls some-package. Jika paket tidak ditemukan, penyebabnya adalah nomor 4.

Langkah perbaikan

Memperbaiki error ini memerlukan pendekatan sistematis per langkah, dari yang paling cepat diterapkan hingga yang memerlukan perubahan konfigurasi arsitektural. Berikut adalah langkah-langkah konkret beserta perintah dan Contoh output.

Langkah 1: Perbaiki resolusi ekstensi berkas (Root cause: ESM membutuhkan ekstensi) Buka berkas yang melempar error, dan tambahkan ekstensi .js pada jalur impor relatif Anda.

Kode Before:

// main.js
import { calculate } from './mathUtils';

Kode After:

// main.js
import { calculate } from './mathUtils.js';

Verifikasi: Jalankan kembali aplikasi Anda. Jika ini satu-satunya penyebabnya, aplikasi akan berjalan tanpa error. Perlu dicatat bahwa Praktik terbaik di ESM adalah selalu menyertakan ekstensi .js untuk berkas JavaScript lokal, meskipun beberapa developer lebih memilih menggunakan ekstensi .mjs secara eksplisit untuk memisahkan modul ESM dan CJS.

Langkah 2: Verifikasi dan koreksi jalur berkas (Root cause: Typo / Refactor lupa update) Kadang-kadang, kita lupa bahwa folder sudah dipindahkan. Gunakan perintah find di terminal untuk mencari lokasi aktual berkas Anda.

# Mencari berkas yang hilang di proyek
find . -name "fileYangDicari.js"

# Contoh output jika berkas ada di folder lain:
# ./src/utils/fileYangDicari.js

Jika berkas ditemukan di ./src/utils/, perbaiki pernyataan import Anda untuk mencerminkan jalur yang benar:

// Perbaikan jalur relatif
import data from './src/utils/fileYangDicari.js';

Verifikasi: Jalankan ulang skrip. Aplikasi seharusnya tidak lagi melempar ERR_MODULE_NOT_FOUND.

Langkah 3: Instal dependensi yang hilang (Root cause: Bare specifier tidak ada di node_modules) Jika error muncul pada impor pihak ketiga (misalnya import axios from 'axios'), kemungkinan paket tersebut tidak terinstal atau terhapus secara tidak sengaja (sering terjadi akibat konflik merge di Git yang menghapus entri di package.json).

# Instal ulang paket yang hilang
npm install axios

# Keluaran yang tampak:
# added 1 package, and audited 150 packages in 3s

Verifikasi: Periksa folder node_modules/axios dan jalankan aplikasi. Jika error berubah menjadi error syntax atau runtime lainnya, berarti modul sudah ditemukan namun ada masalah lain pada kode Anda. Error ERR_MODULE_NOT_FOUND untuk paket ini sudah terpecahkan.

Langkah 4: Gunakan Custom ESM Resolver untuk alias (Solusi Permanen untuk arsitektur kompleks) Jika proyek Anda menggunakan path alias (misalnya import utils from '@utils/helper'), resolver bawaan Node.js ESM tidak akan mengerti alias tersebut (berbeda dengan Webpack/Bundler yang mengolahnya saat build time). Solusi permanennya adalah mendaftarkan custom loader atau menggunakan framework seperti ts-node dengan konfigurasi yang tepat.

Namun, cara paling native tanpa dependensi tambahan adalah menggunakan subpath imports yang didukung langsung oleh Node.js sejak versi 12.19.0.

Ubah package.json Anda:

{
  "type": "module",
  "imports": {
    "#utils/*": "./src/utils/*"
  }
}

Lalu di kode Anda, impor menggunakan tanda pagar (#):

// Kode yang permanen tidak akan error karena alias didefinisikan resmi
import { helper } from '#utils/helper.js';

Verifikasi: Jalankan node index.js. Node.js akan meresolve #utils/helper.js ke ./src/utils/helper.js secara native tanpa error.

Supaya tidak terulang

Mencegah error ERR_MODULE_NOT_FOUND lebih efektif daripada memperbaikinya berulang kali. Pencegahan berakar pada konfigurasi proyek yang ketat dan adopsi Kebiasaan yang baik yang mengeliminasi ambiguitas dalam resolusi modul.

Pencegahan di level konfigurasi: Pertama, deklarasikan secara eksplisit tipe modul yang digunakan proyek Anda di package.json. Dengan menambahkan "type": "module", Anda mengkomitkan seluruh berkas .js di proyek tersebut untuk diperlakukan sebagai ESM. Hal ini mencegah Node.js menebak dan memberikan perilaku yang deterministik. Kedua, jika Anda masih harus menggunakan CommonJS untuk berkas tertentu, gunakan ekstensi .cjs secara eksplisit. Jangan mencampur aduk require dan import tanpa wrapper.

Ketiga, untuk mencegah masalah path alias yang tidak terresolve, selalu gunakan fitur subpath imports ("imports" di package.json) seperti yang dibahas sebelumnya, alih-alih mengandalkan bundler alias yang tidak dihormati oleh runtime Node.js.

Praktik terbaik:

  1. Selalu gunakan ekstensi file saat impor relatif. Meskipun membosankan, menulis import x from './y.js' adalah hukum emas di ESM Node.js.
  2. Hindari penggunaan folder index secara implisit. Di CJS, import './folder' akan otomatis mencari folder/index.js. Di ESM, Anda harus menulisnya secara lengkap: import './folder/index.js'.
  3. Gunakan TypeScript atau ESLint. Tooling modern seperti ESLint dengan plugin import dapat mendeteksi jalur modul yang tidak dapat diresolve sebelum Anda menjalankan kode. TypeScript dengan moduleResolution: "node16" atau "nodenext" juga akan memaksa Anda menulis ekstensi .js pada impor, memberikan early warning saat tahap penulisan kode.

Perbedaan OS yang Kritis: Salah satu jebakan paling mematikan adalah perbedaan sistem berkas antara mesin development (Windows/macOS) dan server production (Linux). Windows dan macOS menggunakan sistem berkas case-insensitive. Artinya, kode berikut akan berjalan mulus di laptop Anda:

// Di Windows, ini akan sukses meskipun file aslinya MyDb.js
import db from './mydb.js'; 

Namun, saat Anda melakukan deploy ke server Linux (case-sensitive), Node.js akan melempar ERR_MODULE_NOT_FOUND karena mydb.js tidak ada; yang ada hanyalah MyDb.js. Untuk mencegah ini, aktifkan opsi assumptions case-sensitivity di tool bundler Anda, atau pada Git, konfigurasikan git config core.ignorecase false agar Git mendeteksi perbedaan kapitalisasi sebagai perubahan yang signifikan. Selalu treat jalur modul Anda sebagai case-sensitive sejak hari pertama development.

Diagnosis lebih dalam

Jika semua solusi di atas telah Anda tempuh—ekstensi sudah ditambahkan, jalur sudah benar, kapitalisasi sudah sesuai, dan node_modules sudah di-reinstall—namun error ERR_MODULE_NOT_FOUND masih menghantui, maka masalah Anda berada pada lapisan yang lebih dalam dari sekadar sistem berkas biasa. Saatnya kita melakukan deep-dive.

Kasus Khusus: Modul Dual CJS/ESM dan Wrapper Seringkali, error ini muncul ketika Anda mencoba mengimpor sebuah paket pihak ketiga yang merupakan modul CommonJS murni, tetapi Anda mengimpornya menggunakan sintaks ESM di proyek Anda. Node.js akan mencari named export pada paket CJS tersebut, dan ketika tidak menemukannya (karena CJS mengekspor menggunakan module.exports = {}), resolver bisa gagal dan melempar error atau memberikan default wrapper yang tidak sesuai harapan.

Cara membaca log untuk kasus ini: Perhatikan baik-baik apakah error mengatakan tidak dapat menemukan berkas, atau tidak dapat menemukan ekspor di dalam berkas. Jika properti url di log menunjukkan bahwa berkas ditemukan (misalnya url: 'file:///node_modules/cjs-pkg/index.js'), masalahnya bukan berkas yang hilang, melainkan struktur ekspor yang tidak kompatibel. Solusi untuk ini adalah menggunakan Dynamic Import:

// Mengimpor CJS ke ESM menggunakan dynamic import
const cjsPkg = await import('cjs-pkg');
const myFunction = cjsPkg.default.myFunction;

Kasus Khusus: Masalah Symlink dan Workspace Jika Anda menggunakan monorepo (seperti pnpm workspaces atau Yarn workspaces), dependensi sering kali di-resolve melalui symlink (tautan simbolis). Terkadang, resolver Node.js gagal mengikuti symlink dengan benar, terutama jika ada isolasi yang ketat (node-linker=isolated). Jalankan perintah berikut untuk memastikan paket terhubung secara fisik:

# Di pnpm, cek apakah paket ada di virtual store
pnpm list <package-name> --depth 0

# Jika perlu, ubah struktur node_modules menjadi flat tanpa symlink
pnpm config set node-linker hoisted

Menggunakan Flag Diagnostik Node.js: Node.js menyediakan environment variable khusus untuk melacak bagaimana resolver modul bekerja di balik layar. Ini adalah senjata paling ampuh untuk diagnosis mendalam.

# Menjalankan Node dengan debug resolver modul
NODE_DEBUG=module node index.js

# Contoh Output yang akan membantu:
# MODULE 30711: looking for "/path/ke/proyek-anda/fileYangDicari.js"
# MODULE 30711:   stat "/path/ke/proyek-anda/fileYangDicari.js" => errno=-2

Perhatikan bagian errno=-2. Ini adalah kode sistem operasi yang berarti ENOENT (Error NO ENTity), yakni berkas atau direktori tidak ada. Dari log ini, Anda bisa melihat path absolut yang sebenarnya sedang dicari oleh Node.js, sehingga Anda bisa membandingkannya secara presisi dengan struktur folder fisik Anda. Terkadang, basePath yang digunakan oleh resolver tidak sesuai dengan ekspektasi Anda, terutama jika Anda menjalankan skrip dari direktori yang berbeda menggunakan cd && node atau runner pihak ketiga.

Bacaan resmi terkait: Jika Anda masih terjebak, dokumenti resmi Node.js adalah tempat terbaik untuk memahami algoritma resolusi secara utuh. Baca spesifikasi resmi di Node.js Module ECMAScript documentation. Perhatikan baik-baik bagian Resolution Algorithm yang menjelaskan secara pseudo-code bagaimana Node.js mengubah specifier string menjadi URL berkas absolut. Selain itu, untuk memahami perbedaan mendalam antara CJS dan ESM dari perspektif desain, package.json exports adalah kunci utama. Baca dokumentasi tentang Conditional Exports. Banyak paket modern menggunakan conditional exports untuk menyediakan berkas berbeda tergantung apakah konsumen menggunakan require() atau import(). Jika paket tersebut salah mengkonfigurasi exports map-nya (misalnya, mendefinisikan import condition tapi lupa mendefinisikan default), resolver Node.js Anda akan memuntahkan ERR_MODULE_NOT_FOUND walau berkas fisiknya ada di dalam folder paket tersebut. Dalam kasus ini, masalah ada di pihak penulis paket, dan Anda mungkin perlu membuat fork atau menggunakan versi paket yang berbeda.