ERROR-HANDLING CODE-QUALITY DEBUGGING JAVASCRIPT TYPESCRIPT BACKEND FRONTEND BEST-PRACTICES SOFTWARE-DEVELOPMENT ROBUSTNESS MAINTAINABILITY DEVELOPER-EXPERIENCE SYSTEM-DESIGN

Mengelola Error di Aplikasi Web Modern: Strategi Praktis untuk Kode yang Robust dan Mudah Didebug

⏱️ 11 menit baca
👨‍💻

Mengelola Error di Aplikasi Web Modern: Strategi Praktis untuk Kode yang Robust dan Mudah Didebug

Sebagai seorang developer, kita pasti pernah bertemu dengan error. Itu adalah bagian tak terpisahkan dari dunia pemrograman. Namun, bagaimana kita mengelola error tersebut di dalam aplikasi kita bisa sangat memengaruhi stabilitas, pengalaman pengguna, dan bahkan produktivitas tim saat debugging.

Banyak dari kita mungkin hanya mengandalkan try...catch dan berharap yang terbaik. Tapi, tahukah Anda bahwa ada strategi yang lebih terstruktur dan efektif untuk menangani error? Artikel ini akan membawa Anda menyelami praktik terbaik dalam mendesain dan mengimplementasikan penanganan error di aplikasi web modern, baik di sisi backend maupun frontend. Mari kita bangun aplikasi yang lebih tangguh dan mudah di-debug!

1. Pendahuluan

Error adalah sinyal. Sinyal bahwa ada sesuatu yang tidak berjalan sesuai harapan. Mengabaikan sinyal ini seperti mengabaikan lampu peringatan di dashboard mobil Anda—bisa berakibat fatal. Penanganan error yang buruk dapat menyebabkan:

🎯 Tujuan kita adalah tidak hanya mencegah aplikasi crash, tetapi juga memberikan informasi yang relevan kepada pengguna (jika perlu) dan detail yang cukup kepada developer untuk bisa memperbaiki masalah dengan cepat. Ini bukan hanya tentang try...catch, tapi tentang filosofi dan arsitektur penanganan error di seluruh codebase Anda.

2. Memahami Jenis-Jenis Error: Operational vs. Programmer Errors

Sebelum kita mulai menulis kode, penting untuk memahami dua kategori utama error:

  1. Operational Errors: Ini adalah error yang bisa diprediksi dan biasanya terjadi karena masalah di lingkungan runtime atau input pengguna.

    • Contoh: Koneksi database terputus, API eksternal tidak merespons, file tidak ditemukan, validasi input gagal (misal: email tidak valid), kuota API terlampaui.
    • Karakteristik: Aplikasi masih bisa berfungsi, dan error ini bisa ditangani secara elegan (misal: tampilkan pesan ‘coba lagi nanti’, ‘input tidak valid’).
    • Penanganan: Kita bisa dan harus menangani error jenis ini secara programmatis.
  2. Programmer Errors: Ini adalah bug atau kesalahan logika di dalam kode kita sendiri.

    • Contoh: Mengakses properti undefined, typo dalam nama variabel, array out of bounds, logic error yang menyebabkan infinite loop.
    • Karakteristik: Tidak terprediksi, menunjukkan adanya cacat dalam kode, dan seringkali tidak bisa ditangani secara programmatis karena menandakan kondisi yang tidak seharusnya terjadi.
    • Penanganan: Idealnya, error ini harus dicegah melalui testing yang ketat dan code review. Jika terjadi di produksi, mereka harus di-log secara detail dan memicu peringatan untuk developer agar segera diperbaiki.

Membedakan kedua jenis ini sangat krusial. ✅ Kita ingin menangani operational errors dengan baik dan mencegah atau dengan cepat mendeteksi programmer errors.

3. Beyond try...catch: Custom Error Classes untuk Konteks Lebih Kaya

Error bawaan JavaScript (Error, TypeError, ReferenceError, dll.) memang berfungsi, tetapi seringkali kurang memberikan konteks yang kita butuhkan. Bayangkan Anda menerima error “Something went wrong”. Apa yang salah? Di mana? Kenapa?

Di sinilah custom error classes berperan. Dengan membuat kelas error kustom, kita bisa:

📌 Praktik Terbaik: Buatlah kelas error dasar yang bisa diperluas untuk semua operational errors Anda.

// app-error.ts
export class AppError extends Error {
    public readonly statusCode: number;
    public readonly isOperational: boolean;
    public readonly details?: Record<string, any>; // Detail tambahan untuk debugging

    constructor(message: string, statusCode: number, isOperational: boolean = true, details?: Record<string, any>) {
        super(message);
        this.name = this.constructor.name; // Penting untuk stack trace yang bersih
        this.statusCode = statusCode;
        this.isOperational = isOperational;
        this.details = details;

        // Memastikan stack trace tetap akurat
        Error.captureStackTrace(this, this.constructor);
    }
}

export class NotFoundError extends AppError {
    constructor(message: string = "Resource not found", details?: Record<string, any>) {
        super(message, 404, true, details);
    }
}

export class ValidationError extends AppError {
    constructor(message: string = "Invalid input provided", details?: Record<string, any>) {
        super(message, 400, true, details);
    }
}

export class UnauthorizedError extends AppError {
    constructor(message: string = "Authentication required", details?: Record<string, any>) {
        super(message, 401, true, details);
    }
}

Contoh Penggunaan:

import { NotFoundError, ValidationError, AppError } from './app-error';

function findUserById(id: string) {
    if (id === "user123") {
        return { id: "user123", name: "Alice" };
    }
    throw new NotFoundError(`User with ID ${id} not found.`);
}

function processOrder(amount: number, currency: string) {
    if (amount <= 0) {
        throw new ValidationError("Order amount must be positive.", { minAmount: 1 });
    }
    if (currency !== "IDR" && currency !== "USD") {
        throw new ValidationError("Unsupported currency.", { supportedCurrencies: ["IDR", "USD"] });
    }
    return { orderId: "ORD-XYZ", status: "processed" };
}

try {
    const user = findUserById("user456");
    console.log(user);
} catch (error) {
    if (error instanceof NotFoundError) {
        console.warn(`📌 User lookup failed: ${error.message}. Status: ${error.statusCode}`);
    } else if (error instanceof AppError) {
        console.error(`⚠️ An operational error occurred: ${error.message}. Status: ${error.statusCode}`);
    } else {
        console.error(`❌ An unexpected system error occurred: ${error.message}`);
    }
}

try {
    const order = processOrder(-100, "JPY");
    console.log(order);
} catch (error) {
    if (error instanceof ValidationError) {
        console.warn(`📌 Validation failed for order: ${error.message}. Details:`, error.details);
    } else {
        console.error(`❌ An unexpected error during order processing: ${error.message}`);
    }
}

Dengan custom error, kita bisa menangani error dengan lebih spesifik dan memberikan feedback yang lebih baik, baik untuk pengguna maupun untuk tim developer.

4. Strategi Propagasi Error: Kapan Melempar, Kapan Menangani

Salah satu keputusan penting dalam penanganan error adalah apakah kita harus menangani error di tempat ia terjadi, atau melemparkannya ke atas (propagate) agar ditangani oleh lapisan aplikasi yang lebih tinggi.

Hindari “Swallowing” Errors: Jangan pernah menangkap error tanpa melakukan apa-apa atau hanya log tanpa tindakan lebih lanjut, kecuali jika Anda memang sengaja ingin mengabaikannya dan memahami risikonya. Ini adalah resep untuk bug yang sulit ditemukan.

Contoh di Backend (Node.js/Express):

// services/user-service.ts
import { NotFoundError } from '../app-error';
import { User } from '../models/user';

export async function getUserFromDB(userId: string): Promise<User> {
    const user = await User.findById(userId); // Asumsi ini fungsi ORM
    if (!user) {
        throw new NotFoundError(`User with ID ${userId} not found in database.`);
    }
    return user;
}

// controllers/user-controller.ts
import { Request, Response, NextFunction } from 'express';
import { getUserFromDB } from '../services/user-service';

export async function getUserProfile(req: Request, res: Response, next: NextFunction) {
    try {
        const userId = req.params.id;
        const user = await getUserFromDB(userId);
        res.status(200).json(user);
    } catch (error) {
        // Jika error adalah AppError (misal NotFoundError), kita kirim ke error handler global
        // Jika bukan, error handler global akan menganggapnya sebagai error tak terduga (programmer error)
        next(error);
    }
}

// app.ts (Global Error Handler)
import express, { Request, Response, NextFunction } from 'express';
import { AppError } from './app-error'; // Path ke file AppError Anda
import winston from 'winston'; // Contoh logger

const app = express();
const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [new winston.transports.Console()],
});

// ... (middleware lainnya)

app.get('/users/:id', getUserProfile);

// ✅ Global Error Handler - ini adalah 'catch-all' untuk semua error yang di-propagate
app.use((err: any, req: Request, res: Response, next: NextFunction) => {
    if (err instanceof AppError) {
        // Ini adalah operational error yang kita definisikan
        logger.warn({
            message: err.message,
            stack: err.stack,
            statusCode: err.statusCode,
            requestUrl: req.originalUrl,
            method: req.method,
            details: err.details,
            isOperational: err.isOperational,
        });
        return res.status(err.statusCode).json({
            status: "error",
            message: err.message,
            code: err.name, // Nama kelas error (e.g., NotFoundError)
            details: err.details,
        });
    }

    // Ini kemungkinan besar adalah programmer error atau error tak terduga lainnya
    logger.error({
        message: err.message,
        stack: err.stack,
        statusCode: 500, // Default untuk error tak terduga
        requestUrl: req.originalUrl,
        method: req.method,
    });
    res.status(500).json({
        status: "error",
        message: "Something went wrong! Please try again later.", // Pesan generik untuk pengguna
    });
});

Dalam contoh di atas, getUserFromDB melemparkan NotFoundError karena itu adalah tanggung jawabnya untuk memvalidasi keberadaan user. getUserProfile menangkap error tersebut dan melemparkannya lagi (next(error)) ke global error handler, yang kemudian bertanggung jawab mengirim respons HTTP yang sesuai dan melakukan logging.

5. Penanganan Error Global dan UI yang Ramah Pengguna

Setelah error di-propagate, perlu ada mekanisme global untuk menangkapnya dan memberikan respons yang konsisten.

Backend: Global Error Handling Middleware

Seperti yang ditunjukkan di contoh sebelumnya, backend biasanya memiliki middleware error handling yang bertindak sebagai “jaring pengaman” terakhir. Middleware ini akan:

Frontend: Error Boundaries dan Fallback UI

Di sisi frontend, terutama dengan framework seperti React, error yang tidak tertangkap bisa merusak seluruh UI. Error Boundaries adalah konsep yang sangat berguna di React untuk menangani error di komponen child dan menampilkan UI fallback yang lebih ramah pengguna.

💡 Ide: Jika sebuah komponen mengalami crash, Error Boundary akan menangkapnya dan menampilkan “Ada masalah teknis, mohon coba lagi” daripada membiarkan seluruh aplikasi crash.

// components/ErrorBoundary.jsx
import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null, errorInfo: null };
  }

  static getDerivedStateFromError(error) {