I/O #

I/O (Input/Output) operations are about how a program interacts with the outside world — reading and writing files, communicating over the network, receiving user input. In TypeScript, I/O is almost always asynchronous because of JavaScript’s single-threaded nature: a blocking I/O operation would freeze the entire event loop and make the application unresponsive. Node.js uses libuv to delegate I/O operations to the operating system non-blockingly, while browsers use Web APIs. TypeScript adds a type-safety layer on top of all of this — accurate return types, explicit error handling, and generics for response data. Understanding I/O in TypeScript means understanding when async is really needed, how to handle I/O errors correctly, and how streaming works for data too large to load into memory all at once.

Async vs Sync I/O — A Choice That Matters #

Node.js provides two versions of almost every I/O operation: an async (non-blocking) version and a sync (blocking) version. Both have their place:

import * as fs from "fs";
import { readFile, writeFile } from "fs/promises";

// SYNCHRONOUS — Blocks the event loop until done
// When appropriate: one-off CLI scripts, initialization before the server starts
try {
  const isi = fs.readFileSync("konfigurasi.json", "utf-8");
  const konfig = JSON.parse(isi);
  console.log("Konfigurasi dimuat:", konfig);
} catch (err) {
  console.error("Gagal memuat konfigurasi:", err);
  process.exit(1);
}

// ASYNCHRONOUS — Doesn't block the event loop
// When appropriate: almost every case in a server/production application
async function muatKonfigurasi(): Promise<Record<string, unknown>> {
  try {
    const isi = await readFile("konfigurasi.json", "utf-8");
    return JSON.parse(isi) as Record<string, unknown>;
  } catch (err) {
    if ((err as NodeJS.ErrnoException).code === "ENOENT") {
      console.warn("File konfigurasi tidak ditemukan, menggunakan default");
      return {};
    }
    throw err;
  }
}
Never use sync I/O operations inside a server that’s serving requests — readFileSync, writeFileSync, execSync all block the event loop and make the server unable to serve other requests during the operation. Use the async variant or streaming.

File Operations with fs/promises #

fs/promises is the module that exposes all fs functions as Promises — far cleaner than the old callback-based API:

Reading Files #

import { readFile, stat, access, constants } from "fs/promises";
import path from "path";

// Read a text file
async function bacaFileTeks(filePath: string): Promise<string> {
  try {
    return await readFile(filePath, "utf-8");
  } catch (err) {
    const error = err as NodeJS.ErrnoException;
    switch (error.code) {
      case "ENOENT":
        throw new Error(`File tidak ditemukan: ${filePath}`);
      case "EACCES":
        throw new Error(`Tidak ada izin membaca: ${filePath}`);
      default:
        throw new Error(`Gagal membaca file: ${error.message}`);
    }
  }
}

// Read a JSON file with type safety
async function bacaJSON<T>(filePath: string): Promise<T> {
  const isi = await readFile(filePath, "utf-8");
  try {
    return JSON.parse(isi) as T;
  } catch {
    throw new SyntaxError(`File ${filePath} bukan JSON yang valid`);
  }
}

// Read a binary file (images, PDFs, etc.)
async function bacaFileBiner(filePath: string): Promise<Buffer> {
  return readFile(filePath); // Without encoding = Buffer
}

// Check whether a file exists before reading
async function fileAda(filePath: string): Promise<boolean> {
  try {
    await access(filePath, constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

// Get file information
async function infoFile(filePath: string) {
  const info = await stat(filePath);
  return {
    ukuran: info.size,
    dibuatPada: info.birthtime,
    dimodifikasiPada: info.mtime,
    adalahFile: info.isFile(),
    adalahDirektori: info.isDirectory(),
  };
}

Writing Files #

import { writeFile, appendFile, mkdir, copyFile } from "fs/promises";

// Write a file (create new or overwrite existing)
async function tulisFile(filePath: string, isi: string): Promise<void> {
  // Make sure the directory exists before writing
  await mkdir(path.dirname(filePath), { recursive: true });
  await writeFile(filePath, isi, "utf-8");
}

// Write an object as formatted JSON
async function tulisJSON<T>(filePath: string, data: T): Promise<void> {
  const json = JSON.stringify(data, null, 2); // 2-space indentation
  await tulisFile(filePath, json);
}

// Append to the end of a file (for logging, csv, etc.)
async function tambahKeFile(filePath: string, baris: string): Promise<void> {
  await appendFile(filePath, baris + "\n", "utf-8");
}

// Write with a backup — rename the old file, write the new one
async function tulisDenganBackup(
  filePath: string,
  isi: string
): Promise<void> {
  const backupPath = `${filePath}.bak`;

  if (await fileAda(filePath)) {
    await copyFile(filePath, backupPath);
  }

  await writeFile(filePath, isi, "utf-8");
}

Directory Operations #

import { readdir, rm, rename } from "fs/promises";

// List all files in a directory with a filter
async function daftarFile(
  dirPath: string,
  ekstensi?: string
): Promise<string[]> {
  const entri = await readdir(dirPath, { withFileTypes: true });

  return entri
    .filter((e) => e.isFile())
    .map((e) => e.name)
    .filter((nama) => !ekstensi || nama.endsWith(ekstensi));
}

// Recursively list all files
async function daftarFilesRekursif(dirPath: string): Promise<string[]> {
  const entri = await readdir(dirPath, { withFileTypes: true });
  const files: string[] = [];

  for (const entri_ of entri) {
    const fullPath = path.join(dirPath, entri_.name);
    if (entri_.isDirectory()) {
      const subFiles = await daftarFilesRekursif(fullPath);
      files.push(...subFiles);
    } else {
      files.push(fullPath);
    }
  }

  return files;
}

// Delete a file or directory
async function hapus(targetPath: string): Promise<void> {
  await rm(targetPath, { recursive: true, force: true });
}

// Rename / move a file
async function pindahkanFile(dari: string, ke: string): Promise<void> {
  await mkdir(path.dirname(ke), { recursive: true });
  await rename(dari, ke);
}

Streaming — For Large Files #

Reading an entire file into memory with readFile doesn’t scale for large files. Streaming processes data chunk by chunk:

import { createReadStream, createWriteStream } from "fs";
import { pipeline } from "stream/promises";
import { Transform } from "stream";

// Read a file stream line by line — efficient for large log files
import * as readline from "readline";

async function prosesFileBesar(filePath: string): Promise<void> {
  const fileStream = createReadStream(filePath, { encoding: "utf-8" });
  const rl = readline.createInterface({
    input: fileStream,
    crlfDelay: Infinity, // Handle Windows line endings (\r\n)
  });

  let nomorBaris = 0;
  for await (const baris of rl) {
    nomorBaris++;
    // Process each line without loading the whole file into memory
    if (baris.includes("ERROR")) {
      console.log(`Baris ${nomorBaris}: ${baris}`);
    }
  }

  console.log(`Selesai memproses ${nomorBaris} baris`);
}

// Stream pipeline: read → transform → write
async function transformasiFile(
  inputPath: string,
  outputPath: string
): Promise<void> {
  const readStream = createReadStream(inputPath, { encoding: "utf-8" });
  const writeStream = createWriteStream(outputPath, { encoding: "utf-8" });

  // Transform stream: modify every chunk
  const uppercase = new Transform({
    transform(chunk: Buffer, _encoding, callback) {
      callback(null, chunk.toString().toUpperCase());
    },
  });

  // pipeline handles errors and cleanup automatically
  await pipeline(readStream, uppercase, writeStream);
  console.log("Transformasi selesai");
}

Safe Path Handling #

Joining paths manually with string concatenation is a bug source — especially on Windows which uses \ as the separator:

import path from "path";

// ANTI-PATTERN: Manual concatenation — not cross-platform
const pathSalah = __dirname + "/data/" + namaFile; // Can break on Windows

// CORRECT: Use path.join or path.resolve
const pathBenar = path.join(__dirname, "data", namaFile);

// path.resolve — resolves to an absolute path
const pathAbsolut = path.resolve("./data", namaFile);

// Useful path utilities
console.log(path.basename("/home/user/file.txt"));        // "file.txt"
console.log(path.basename("/home/user/file.txt", ".txt")); // "file" — without the extension
console.log(path.dirname("/home/user/file.txt"));          // "/home/user"
console.log(path.extname("/home/user/file.txt"));          // ".txt"
console.log(path.parse("/home/user/file.txt"));
// { root: '/', dir: '/home/user', base: 'file.txt', ext: '.txt', name: 'file' }

// Safe path joining
const baseDir = "/data/proyek";
const userInput = "../../etc/passwd"; // Path traversal attack attempt!

// ANTI-PATTERN: Directly joining a path from user input
const pathBerbahaya = path.join(baseDir, userInput);
// Can escape the allowed directory!

// CORRECT: Validate that the path doesn't escape the allowed directory
function pathAman(baseDir: string, userPath: string): string {
  const resolved = path.resolve(baseDir, userPath);
  if (!resolved.startsWith(path.resolve(baseDir))) {
    throw new Error(`Path traversal terdeteksi: ${userPath}`);
  }
  return resolved;
}

Terminal Input with readline #

import * as readline from "readline";
import { promisify } from "util";

// Standard readline interface
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

// Promisify rl.question for async/await
function tanya(pertanyaan: string): Promise<string> {
  return new Promise((resolve) => {
    rl.question(pertanyaan, (jawaban) => {
      resolve(jawaban);
    });
  });
}

// Example: a multi-step interactive dialog
async function dialogPendaftaran(): Promise<void> {
  try {
    const nama = await tanya("Nama lengkap: ");
    const email = await tanya("Email: ");
    const konfirmasi = await tanya(`Daftar sebagai ${nama} <${email}>? (y/n): `);

    if (konfirmasi.toLowerCase() === "y") {
      console.log(`\nTerima kasih telah mendaftar, ${nama}!`);
      // Process the registration...
    } else {
      console.log("\nPendaftaran dibatalkan.");
    }
  } finally {
    rl.close(); // Always close the interface in finally
  }
}

dialogPendaftaran().catch(console.error);

Fetch API — Type-Safe HTTP I/O #

// Types for the API response
interface ResponsePaginasi<T> {
  data: T[];
  total: number;
  halaman: number;
  perHalaman: number;
}

interface Pengguna {
  id: number;
  nama: string;
  email: string;
}

// A type-safe fetch wrapper with correct error handling
async function ambilAPI<T>(
  url: string,
  opsi?: RequestInit
): Promise<T> {
  const response = await fetch(url, {
    headers: { "Content-Type": "application/json" },
    ...opsi,
  });

  if (!response.ok) {
    const pesanError = await response.text().catch(() => "Tidak ada pesan error");
    throw new Error(
      `HTTP ${response.status} ${response.statusText}: ${pesanError}`
    );
  }

  return response.json() as Promise<T>;
}

// GET request
async function ambilPengguna(id: number): Promise<Pengguna> {
  return ambilAPI<Pengguna>(`https://api.example.com/pengguna/${id}`);
}

// POST request
async function buatPengguna(
  data: Omit<Pengguna, "id">
): Promise<Pengguna> {
  return ambilAPI<Pengguna>("https://api.example.com/pengguna", {
    method: "POST",
    body: JSON.stringify(data),
  });
}

// Upload a file with FormData
async function unggahFile(
  file: File,
  deskripsi: string
): Promise<{ url: string }> {
  const formData = new FormData();
  formData.append("file", file);
  formData.append("deskripsi", deskripsi);

  const response = await fetch("/api/upload", {
    method: "POST",
    body: formData,
    // Don't set Content-Type — let the browser set it (with the boundary)
  });

  if (!response.ok) throw new Error(`Upload gagal: ${response.status}`);
  return response.json() as Promise<{ url: string }>;
}

// Fetch with a timeout
async function ambilDenganTimeout<T>(
  url: string,
  timeoutMs: number
): Promise<T> {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    if (!response.ok) throw new Error(`HTTP ${response.status}`);
    return response.json() as Promise<T>;
  } catch (err) {
    if ((err as Error).name === "AbortError") {
      throw new Error(`Request timeout setelah ${timeoutMs}ms`);
    }
    throw err;
  } finally {
    clearTimeout(timeoutId);
  }
}

File Watching — Detecting File Changes #

import { watch } from "fs";
import { stat } from "fs/promises";

// Simple watch — detect file changes
function pantauFile(
  filePath: string,
  callback: (event: string) => void
): () => void {
  const watcher = watch(filePath, { persistent: false }, (event) => {
    callback(event);
  });

  // Return a function to stop watching
  return () => watcher.close();
}

// Recursively watch a directory
function pantauDirektori(
  dirPath: string,
  callback: (event: string, filename: string | null) => void
): () => void {
  const watcher = watch(
    dirPath,
    { recursive: true, persistent: false },
    (event, filename) => {
      callback(event, filename);
    }
  );

  return () => watcher.close();
}

// Usage: reload the configuration when the file changes
const hentikanPantauan = pantauFile("konfigurasi.json", async (event) => {
  if (event === "change") {
    console.log("Konfigurasi berubah, memuat ulang...");
    try {
      const konfig = await bacaJSON<Record<string, unknown>>("konfigurasi.json");
      console.log("Konfigurasi baru:", konfig);
    } catch (err) {
      console.error("Gagal memuat konfigurasi baru:", err);
    }
  }
});

// Stop watching when the application closes
process.on("SIGINT", () => {
  hentikanPantauan();
  process.exit(0);
});

I/O Flow Patterns #

flowchart TD
    A[I/O Operation] --> B{Data Size?}

    B -->|Small < 10MB| C{Type?}
    B -->|Large > 10MB| D["Streaming<br/>createReadStream"]

    C -->|Text/JSON file| E["fs/promises<br/>readFile writeFile"]
    C -->|User input| F["readline<br/>question-answer"]
    C -->|HTTP| G["fetch API<br/>type-safe wrapper"]
    C -->|Binary file| H["fs/promises<br/>without encoding"]

    D --> I["readline for text<br/>pipeline for transforms"]

    E --> J["Error Handling<br/>ErrnoException code"]
    F --> K["Close the interface<br/>in finally"]
    G --> L["Check response.ok<br/>before .json"]

    style D fill:#339af0,color:#fff
    style J fill:#ff6b6b,color:#fff
    style K fill:#fcc419,color:#000
    style L fill:#ff6b6b,color:#fff

Summary #

  • Always use async I/O inside servers or applications serving many users — readFileSync, writeFileSync block the event loop and break concurrency; sync is only for one-off scripts or initialization before the server starts.
  • fs/promises is preferred over fs with callbacks — the Promise-based API works directly with async/await without needing util.promisify, and the code is far cleaner.
  • Always create the directory before writing a file — mkdir(path.dirname(filePath), { recursive: true }) prevents the ENOENT error when the destination directory doesn’t exist yet.
  • Use streaming for large files — createReadStream and readline.Interface process files line by line without loading the whole file into memory; essential for log files, large CSVs, or data dumps.
  • Use path.join() instead of string concatenation — path.join handles the separator differences between Windows (\\) and Unix (/) automatically.
  • Validate paths from user input — always check that path.resolve(base, userInput).startsWith(path.resolve(base)) to prevent path traversal attacks.
  • Check response.ok before response.json() — fetch doesn’t throw errors for 4xx/5xx responses; error statuses can only be detected from the response.ok or response.status properties.
  • Use AbortController to give fetch requests a timeout — without it, a request can hang forever if the server doesn’t respond.
  • Handle NodeJS.ErrnoException with a switch on the .code property — ENOENT (no file), EACCES (no permission), EISDIR (target is a directory) all need different handling.

← Previous: Multi Threading   Next: Socket →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact