I/O #
Input/Output adalah fondasi dari hampir setiap program yang berjalan di dunia nyata — membaca konfigurasi dari file, menulis log, memproses CSV berukuran besar, atau menerima input dari pengguna di terminal. Di Node.js, seluruh operasi I/O bersifat asinkron secara bawaan, yang berarti program kamu tidak perlu berhenti menunggu disk atau jaringan selesai bekerja. TypeScript melapis semua ini dengan sistem tipe yang memastikan kamu menangani return value dan error dengan benar. Artikel ini membahas fs/promises sebagai API utama untuk operasi file, stream untuk data yang terlalu besar untuk dimuat ke memori sekaligus, dan pola-pola penanganan I/O yang aman di production.
Modul fs dan Path #
Node.js menyediakan dua versi API file system — fs berbasis callback dan fs/promises berbasis Promise. Di TypeScript modern, selalu gunakan fs/promises agar bisa memanfaatkan async/await secara penuh.
import { promises as fs } from "fs";
import path from "path";
// atau dengan ES module syntax
import * as fs from "fs/promises";
import { join, resolve, dirname, basename, extname } from "path";
Manipulasi Path #
Selalu gunakan modul path untuk memanipulasi path file — jangan pernah konkatenasi path secara manual dengan string karena perilakunya berbeda di Windows (\) dan Unix (/).
import path from "path";
// ANTI-PATTERN: konkatenasi path dengan string
const filePath = "data" + "/" + "config" + "/" + "app.json"; // ✗ tidak cross-platform
// BENAR: gunakan path.join()
const filePathBenar = path.join("data", "config", "app.json"); // ✓
// path.join — gabungkan segmen path, normalisasi separator
console.log(path.join("/home/user", "documents", "file.txt"));
// "/home/user/documents/file.txt"
console.log(path.join("/home/user", "../", "documents"));
// "/home/documents" — .. di-resolve otomatis
// path.resolve — buat absolute path dari current working directory
console.log(path.resolve("config", "app.json"));
// "/home/user/project/config/app.json" (tergantung cwd)
console.log(path.resolve("/tmp", "config", "app.json"));
// "/tmp/config/app.json" — absolute path tetap absolute
// path.dirname — direktori dari sebuah path
console.log(path.dirname("/home/user/file.txt")); // "/home/user"
// path.basename — nama file dari sebuah path
console.log(path.basename("/home/user/file.txt")); // "file.txt"
console.log(path.basename("/home/user/file.txt", ".txt")); // "file" (tanpa ekstensi)
// path.extname — ekstensi file
console.log(path.extname("document.pdf")); // ".pdf"
console.log(path.extname("archive.tar.gz")); // ".gz"
console.log(path.extname("Makefile")); // "" (tidak ada ekstensi)
// path.parse — pecah path menjadi komponen
const parsed = path.parse("/home/user/document.pdf");
console.log(parsed);
// {
// root: "/",
// dir: "/home/user",
// base: "document.pdf",
// ext: ".pdf",
// name: "document"
// }
// path.format — kebalikan dari parse
console.log(path.format({ dir: "/home/user", name: "document", ext: ".pdf" }));
// "/home/user/document.pdf"
// __dirname di ES module — tidak tersedia langsung, gunakan ini
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
Membaca File #
Baca Seluruh File ke Memori #
Untuk file berukuran kecil hingga menengah, readFile adalah cara paling praktis.
import { promises as fs } from "fs";
import path from "path";
// baca sebagai string (dengan encoding)
async function bacaFileTeks(filePath: string): Promise<string> {
const isiFile = await fs.readFile(filePath, "utf-8");
return isiFile;
}
// baca sebagai Buffer (untuk file biner: gambar, PDF, dll)
async function bacaFileBiner(filePath: string): Promise<Buffer> {
return fs.readFile(filePath); // tanpa encoding — mengembalikan Buffer
}
// baca dan parse JSON — pola yang sangat umum
async function bacaJSON<T>(filePath: string): Promise<T> {
const isi = await fs.readFile(filePath, "utf-8");
return JSON.parse(isi) as T;
}
// contoh penggunaan
interface Konfigurasi {
host: string;
port: number;
database: string;
}
const config = await bacaJSON<Konfigurasi>(
path.join(__dirname, "config", "database.json")
);
console.log(`Terhubung ke ${config.host}:${config.port}`);
// baca banyak file sekaligus — lebih cepat dari loop sequential
async function bacaBanyakFile(paths: string[]): Promise<string[]> {
// Promise.all — semua file dibaca secara paralel
return Promise.all(paths.map((p) => fs.readFile(p, "utf-8")));
}
// ANTI-PATTERN: baca file secara sequential dalam loop
async function bacaFileSatuSatu(paths: string[]): Promise<string[]> {
const hasil: string[] = [];
for (const p of paths) {
hasil.push(await fs.readFile(p, "utf-8")); // ✗ menunggu satu per satu
}
return hasil;
}
Cek Keberadaan File #
// ANTI-PATTERN: cek dengan try/catch pada readFile
async function fileAdaSalah(filePath: string): Promise<boolean> {
try {
await fs.readFile(filePath); // ✗ membaca file hanya untuk cek keberadaannya — boros
return true;
} catch {
return false;
}
}
// BENAR: gunakan access() atau stat()
async function fileAda(filePath: string): Promise<boolean> {
try {
await fs.access(filePath); // ✓ hanya cek aksesibilitas, tidak membaca isi
return true;
} catch {
return false;
}
}
// stat — informasi detail tentang file atau direktori
async function infoFile(filePath: string) {
const stat = await fs.stat(filePath);
return {
ukuranBytes: stat.size,
ukuranKB: (stat.size / 1024).toFixed(2),
isFile: stat.isFile(),
isDirectory: stat.isDirectory(),
dibuat: stat.birthtime,
dimodifikasi: stat.mtime,
diakses: stat.atime,
};
}
Menulis File #
Tulis File Baru atau Timpa #
// writeFile — tulis string atau Buffer ke file
// jika file sudah ada, isinya ditimpa seluruhnya
async function tulisFile(filePath: string, konten: string): Promise<void> {
// buat direktori jika belum ada
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, konten, "utf-8");
}
// tulis JSON dengan formatting
async function tulisJSON(filePath: string, data: unknown): Promise<void> {
const json = JSON.stringify(data, null, 2); // indentasi 2 spasi
await fs.writeFile(filePath, json, "utf-8");
}
// tulis Buffer (file biner)
async function tulisBiner(filePath: string, data: Buffer): Promise<void> {
await fs.writeFile(filePath, data);
}
// writeFile dengan opsi tambahan
await fs.writeFile("output.txt", "isi file", {
encoding: "utf-8",
flag: "w", // "w" = tulis (default), "a" = append, "wx" = tulis, error jika sudah ada
mode: 0o644, // permission Unix: owner rw, group r, others r
});
Append — Tambah ke File yang Ada #
// appendFile — tambah konten di akhir file
// membuat file baru jika belum ada
async function tambahLog(
logPath: string,
pesan: string,
level: "INFO" | "WARN" | "ERROR" = "INFO"
): Promise<void> {
const timestamp = new Date().toISOString();
const baris = `[${timestamp}] [${level}] ${pesan}\n`;
await fs.appendFile(logPath, baris, "utf-8");
}
// penggunaan
await tambahLog("app.log", "Server dimulai pada port 3000");
await tambahLog("app.log", "Koneksi database gagal", "ERROR");
Tulis Atomik — Mencegah Korupsi Data #
Menulis langsung ke file target berisiko — jika proses terhenti di tengah penulisan, file bisa rusak. Pola yang aman adalah menulis ke file sementara dulu, lalu rename.
import { randomUUID } from "crypto";
// ANTI-PATTERN: tulis langsung ke file target
async function simpanKonfigSalah(filePath: string, data: unknown): Promise<void> {
await fs.writeFile(filePath, JSON.stringify(data, null, 2)); // ✗ jika crash di sini, file rusak
}
// BENAR: tulis atomik via file sementara + rename
async function simpanKonfigAman(filePath: string, data: unknown): Promise<void> {
const tmpPath = `${filePath}.${randomUUID()}.tmp`;
try {
// tulis ke file sementara
await fs.writeFile(tmpPath, JSON.stringify(data, null, 2), "utf-8");
// rename atomik — di Unix, rename() adalah operasi atomik
await fs.rename(tmpPath, filePath);
} catch (error) {
// bersihkan file sementara jika ada error
await fs.unlink(tmpPath).catch(() => {}); // abaikan error jika tmp tidak ada
throw error;
}
}
Operasi Direktori #
// buat direktori (termasuk parent yang belum ada)
await fs.mkdir("data/logs/2024", { recursive: true });
// recursive: true — tidak error jika direktori sudah ada
// baca isi direktori
async function listFile(dirPath: string): Promise<string[]> {
return fs.readdir(dirPath);
}
// baca dengan tipe — membedakan file dan subdirektori
async function listDetail(dirPath: string) {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
return {
files: entries
.filter((e) => e.isFile())
.map((e) => e.name),
directories: entries
.filter((e) => e.isDirectory())
.map((e) => e.name),
};
}
// baca direktori secara rekursif — list semua file di semua subdirektori
async function listFileRekursif(dirPath: string): Promise<string[]> {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const files: string[] = [];
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
const subFiles = await listFileRekursif(fullPath);
files.push(...subFiles);
} else {
files.push(fullPath);
}
}
return files;
}
// hapus direktori beserta isinya
await fs.rm("data/tmp", { recursive: true, force: true });
// force: true — tidak error jika direktori tidak ada
// salin file
await fs.copyFile("source.txt", "destination.txt");
// jika destination sudah ada, akan ditimpa
// pindah / rename file atau direktori
await fs.rename("old-name.txt", "new-name.txt");
// hapus file
await fs.unlink("file-yang-dihapus.txt");
// buat symlink
await fs.symlink("target-path", "link-path");
// baca symlink — path yang ditunjuk
const target = await fs.readlink("link-path");
Stream — Memproses File Besar #
Saat ukuran file bisa mencapai ratusan MB atau lebih, membaca seluruh file ke memori dengan readFile adalah anti-pattern — program bisa kehabisan RAM. Stream memungkinkan memproses data sepotong-sepotong (chunk) tanpa harus memuat semuanya sekaligus.
flowchart LR
A[(File 500MB)] --> B[ReadStream\nchunk 64KB]
B --> C[Transform\nproses chunk]
C --> D[WriteStream\ntulis chunk]
D --> E[(Output File)]
style A fill:#374151,color:#fff
style E fill:#374151,color:#fffMembaca dengan Stream #
import { createReadStream } from "fs";
import { createInterface } from "readline";
// baca file teks baris per baris — sangat efisien untuk file log atau CSV besar
async function* bacaPerBaris(filePath: string): AsyncGenerator<string> {
const fileStream = createReadStream(filePath, { encoding: "utf-8" });
const rl = createInterface({
input: fileStream,
crlfDelay: Infinity, // tangani line ending Windows (\r\n)
});
for await (const line of rl) {
yield line;
}
}
// proses CSV besar tanpa memuat ke memori
async function prosesCSVBesar(csvPath: string): Promise<void> {
let barisKe = 0;
let header: string[] = [];
for await (const baris of bacaPerBaris(csvPath)) {
barisKe++;
if (barisKe === 1) {
header = baris.split(",").map((h) => h.trim());
continue; // lewati baris header
}
const nilai = baris.split(",");
const record = Object.fromEntries(
header.map((key, i) => [key, nilai[i]?.trim() ?? ""])
);
// proses satu record — tidak perlu simpan semua ke memori
await prosesRecord(record);
if (barisKe % 10000 === 0) {
console.log(`Diproses: ${barisKe.toLocaleString()} baris`);
}
}
console.log(`Selesai: total ${barisKe.toLocaleString()} baris`);
}
async function prosesRecord(record: Record<string, string>): Promise<void> {
// implementasi pemrosesan satu record
}
Menulis dengan Stream #
import { createWriteStream } from "fs";
import { Writable } from "stream";
// tulis data besar ke file secara streaming
async function tulisStreamCSV(
outputPath: string,
data: AsyncGenerator<Record<string, unknown>>,
header: string[]
): Promise<void> {
const writeStream = createWriteStream(outputPath, { encoding: "utf-8" });
// tulis header
writeStream.write(header.join(",") + "\n");
for await (const row of data) {
const line = header.map((key) => {
const val = String(row[key] ?? "");
// escape nilai yang mengandung koma atau newline
return val.includes(",") || val.includes("\n") ? `"${val.replace(/"/g, '""')}"` : val;
}).join(",") + "\n";
// write() mengembalikan false jika buffer penuh — tunggu drain sebelum lanjut
const dapatLanjut = writeStream.write(line);
if (!dapatLanjut) {
await new Promise<void>((resolve) => writeStream.once("drain", resolve));
}
}
// tunggu sampai semua data selesai ditulis
await new Promise<void>((resolve, reject) => {
writeStream.end((err?: Error | null) => {
if (err) reject(err);
else resolve();
});
});
}
Pipeline — Menghubungkan Stream #
pipeline dari modul stream/promises menghubungkan beberapa stream dan menangani error serta cleanup secara otomatis — jauh lebih aman dari menghubungkan stream secara manual.
import { pipeline } from "stream/promises";
import { createReadStream, createWriteStream } from "fs";
import { createGzip, createGunzip } from "zlib";
import { Transform } from "stream";
// kompres file dengan gzip
async function kompresFile(input: string, output: string): Promise<void> {
await pipeline(
createReadStream(input),
createGzip(),
createWriteStream(output)
);
console.log(`File dikompres: ${input} → ${output}`);
}
// dekompresi file gzip
async function dekompresFile(input: string, output: string): Promise<void> {
await pipeline(
createReadStream(input),
createGunzip(),
createWriteStream(output)
);
}
// transform stream kustom — ubah setiap baris menjadi uppercase
function buatUppercaseTransform(): Transform {
return new Transform({
encoding: "utf-8",
transform(chunk, encoding, callback) {
callback(null, chunk.toString().toUpperCase());
},
});
}
// pipeline dengan transform kustom
async function prosesFileUppercase(input: string, output: string): Promise<void> {
await pipeline(
createReadStream(input, { encoding: "utf-8" }),
buatUppercaseTransform(),
createWriteStream(output)
);
}
stdin dan stdout #
Baca Input dari Terminal #
import { createInterface } from "readline";
// baca satu baris input dari pengguna
async function tanya(pertanyaan: string): Promise<string> {
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
return new Promise((resolve) => {
rl.question(pertanyaan, (jawaban) => {
rl.close();
resolve(jawaban.trim());
});
});
}
// baca input dengan validasi
async function tanyaDenganValidasi(
pertanyaan: string,
validasi: (input: string) => boolean,
pesanError: string
): Promise<string> {
while (true) {
const jawaban = await tanya(pertanyaan);
if (validasi(jawaban)) return jawaban;
console.error(pesanError);
}
}
// contoh: CLI sederhana untuk input data
async function inputDataUser(): Promise<void> {
const nama = await tanyaDenganValidasi(
"Masukkan nama: ",
(input) => input.length >= 2,
"Nama minimal 2 karakter."
);
const emailInput = await tanyaDenganValidasi(
"Masukkan email: ",
(input) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input),
"Format email tidak valid."
);
const umurStr = await tanyaDenganValidasi(
"Masukkan umur: ",
(input) => !isNaN(Number(input)) && Number(input) > 0,
"Umur harus berupa angka positif."
);
console.log("\nData yang dimasukkan:");
console.log(`Nama : ${nama}`);
console.log(`Email : ${emailInput}`);
console.log(`Umur : ${umurStr} tahun`);
}
// baca stdin line by line (untuk piping: cat file.txt | node script.js)
async function bacaStdinPerBaris(): Promise<string[]> {
const baris: string[] = [];
const rl = createInterface({ input: process.stdin });
for await (const line of rl) {
baris.push(line);
}
return baris;
}
Menulis ke stdout dan stderr #
// console.log — menulis ke stdout dengan newline
console.log("Pesan biasa ke stdout");
// process.stdout.write — menulis tanpa newline otomatis
process.stdout.write("Loading");
process.stdout.write(".");
process.stdout.write(".");
process.stdout.write(". Selesai!\n");
// console.error — menulis ke stderr
console.error("Ini pesan error ke stderr");
// process.stderr.write — menulis ke stderr tanpa newline
process.stderr.write("Error: koneksi gagal\n");
// progress bar sederhana di terminal
async function prosesLambat(items: string[]): Promise<void> {
const total = items.length;
for (let i = 0; i < total; i++) {
await prosesSatuItem(items[i]);
const persen = Math.round(((i + 1) / total) * 100);
const terisi = Math.round(persen / 2);
const kosong = 50 - terisi;
const bar = "█".repeat(terisi) + "░".repeat(kosong);
// \r — kembali ke awal baris tanpa newline (overwrite baris yang sama)
process.stdout.write(`\r[${bar}] ${persen}% (${i + 1}/${total})`);
}
process.stdout.write("\n"); // newline setelah selesai
}
async function prosesSatuItem(_item: string): Promise<void> {
await new Promise((r) => setTimeout(r, 50)); // simulasi proses
}
Menonton Perubahan File #
import { watch } from "fs/promises";
// watch — pantau perubahan file atau direktori
async function pantauPerubahan(targetPath: string): Promise<void> {
console.log(`Memantau perubahan pada: ${targetPath}`);
const watcher = watch(targetPath, { recursive: true });
for await (const event of watcher) {
console.log(`Event: ${event.eventType}, File: ${event.filename}`);
if (event.eventType === "change" && event.filename?.endsWith(".json")) {
console.log("File JSON berubah — reload konfigurasi...");
// reload konfigurasi
}
}
}
// hot reload konfigurasi — pola umum di aplikasi long-running
class KonfigurasiHotReload {
private config: Record<string, unknown> = {};
private configPath: string;
constructor(configPath: string) {
this.configPath = configPath;
}
async muat(): Promise<void> {
const isi = await fs.readFile(this.configPath, "utf-8");
this.config = JSON.parse(isi);
console.log("Konfigurasi dimuat");
}
async mulaiWatch(): Promise<void> {
await this.muat();
const watcher = watch(this.configPath);
for await (const event of watcher) {
if (event.eventType === "change") {
try {
await this.muat();
console.log("Konfigurasi di-reload");
} catch (error) {
console.error("Gagal reload konfigurasi:", error);
// biarkan konfigurasi lama tetap aktif
}
}
}
}
get<T>(key: string): T {
return this.config[key] as T;
}
}
Penanganan Error I/O #
Error I/O di Node.js memiliki kode errno yang bisa digunakan untuk menentukan penanganan yang tepat.
import { constants } from "fs";
// kode error yang paling umum
// ENOENT — file atau direktori tidak ditemukan
// EACCES — tidak punya izin akses
// EEXIST — file/direktori sudah ada
// EISDIR — target adalah direktori, bukan file
// ENOSPC — disk penuh
// EMFILE — terlalu banyak file terbuka
async function bacaFileAman(filePath: string): Promise<string | null> {
try {
return await fs.readFile(filePath, "utf-8");
} catch (error) {
if (error instanceof Error && "code" in error) {
const kodeError = (error as NodeJS.ErrnoException).code;
switch (kodeError) {
case "ENOENT":
console.warn(`File tidak ditemukan: ${filePath}`);
return null;
case "EACCES":
console.error(`Tidak punya izin membaca: ${filePath}`);
return null;
case "EISDIR":
console.error(`Target adalah direktori, bukan file: ${filePath}`);
return null;
default:
console.error(`Error membaca file (${kodeError}): ${filePath}`);
throw error; // lempar ulang error yang tidak dikenal
}
}
throw error;
}
}
// helper untuk operasi yang mungkin gagal karena race condition
async function tulisFileDenganRetry(
filePath: string,
konten: string,
maxRetry = 3
): Promise<void> {
for (let i = 0; i < maxRetry; i++) {
try {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, konten, "utf-8");
return;
} catch (error) {
if (error instanceof Error && "code" in error) {
const kode = (error as NodeJS.ErrnoException).code;
if (kode === "ENOSPC") throw error; // disk penuh — tidak ada gunanya retry
if (i === maxRetry - 1) throw error; // sudah retry maksimal
}
// tunggu sebentar sebelum retry
await new Promise((r) => setTimeout(r, 100 * (i + 1)));
}
}
}
// cek izin akses sebelum operasi
async function cekIzinBaca(filePath: string): Promise<boolean> {
try {
await fs.access(filePath, constants.R_OK);
return true;
} catch {
return false;
}
}
async function cekIzinTulis(filePath: string): Promise<boolean> {
try {
await fs.access(filePath, constants.W_OK);
return true;
} catch {
return false;
}
}
Jangan simpan path yang berasal dari input pengguna secara langsung tanpa validasi — ini membuka celah path traversal attack. Selalu gunakan
path.resolve()dan verifikasi bahwa path hasil resolusi masih berada di dalam direktori yang diizinkan.function validasiPath(userInput: string, baseDirAman: string): string { const resolved = path.resolve(baseDirAman, userInput); if (!resolved.startsWith(baseDirAman)) { throw new Error("Akses ditolak: path di luar direktori yang diizinkan"); } return resolved; }
Pola Umum di Aplikasi Nyata #
Rotasi Log File #
async function rotasiLog(
logDir: string,
namaBase: string,
maksFile: number = 7
): Promise<void> {
const files = await fs.readdir(logDir);
const logFiles = files
.filter((f) => f.startsWith(namaBase) && f.endsWith(".log"))
.sort()
.reverse(); // terbaru dulu
// hapus file log yang melebihi batas
for (const file of logFiles.slice(maksFile - 1)) {
await fs.unlink(path.join(logDir, file));
console.log(`Log lama dihapus: ${file}`);
}
// rename log aktif ke log dengan timestamp
const logAktif = path.join(logDir, `${namaBase}.log`);
if (await fileAda(logAktif)) {
const timestamp = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
await fs.rename(logAktif, path.join(logDir, `${namaBase}-${timestamp}.log`));
}
}
Baca dan Tulis File Secara Paralel dengan Batas Concurrency #
// memproses banyak file secara paralel tapi dengan batas concurrency
// mencegah membuka terlalu banyak file descriptor sekaligus
async function prosesFileDenganLimit<T>(
filePaths: string[],
handler: (path: string) => Promise<T>,
concurrency: number = 10
): Promise<T[]> {
const hasil: T[] = [];
const antrian = [...filePaths];
async function worker(): Promise<void> {
while (antrian.length > 0) {
const filePath = antrian.shift()!;
hasil.push(await handler(filePath));
}
}
// jalankan N worker secara paralel
await Promise.all(
Array.from({ length: Math.min(concurrency, filePaths.length) }, worker)
);
return hasil;
}
// contoh: proses 1000 file JSON tapi maksimal 10 dibuka bersamaan
const semuaHasil = await prosesFileDenganLimit(
await listFileRekursif("./data"),
async (filePath) => {
if (!filePath.endsWith(".json")) return null;
return bacaJSON(filePath);
},
10
);
Ringkasan #
- Selalu gunakan
fs/promisesdenganasync/await— hindari versi callback (fs.readFile(path, cb)) yang membuat kode sulit dibaca dan error-prone.- Gunakan
path.join()danpath.resolve()untuk memanipulasi path — jangan konkatenasi string secara manual karena tidak cross-platform.Promise.alluntuk operasi paralel — membaca banyak file sekaligus jauh lebih cepat dari loop sequential denganawaitdi dalam.- Stream untuk file besar — gunakan
readlineuntuk baca per baris danpipeline()daristream/promisesuntuk menghubungkan stream dengan penanganan error yang benar.- Tulis atomik via file sementara + rename — mencegah korupsi file jika proses terhenti di tengah penulisan.
- Tangkap
errnountuk error yang spesifik —ENOENT(tidak ada),EACCES(tidak punya izin),ENOSPC(disk penuh) memerlukan penanganan yang berbeda.- Validasi path dari input pengguna — selalu gunakan
path.resolve()dan cek bahwa hasilnya masih dalam direktori yang aman untuk mencegah path traversal attack.- Gunakan
fs.access()bukanreadFileuntuk cek keberadaan file — lebih efisien karena tidak membaca isi file.- Batasi concurrency saat memproses banyak file — membuka terlalu banyak file descriptor sekaligus bisa menyebabkan error
EMFILE.