Cara Menangani Error dan HTTP Status Code yang Benar

Tutorial
Jul 18, 2026
8 mnt baca
14 tayangan
Cara Menangani Error dan HTTP Status Code yang Benar
A

Admin

Penulis Artikel

Cara Menangani Error dan HTTP Status Code yang Benar di Express.js

Di artikel sebelumnya, kita sudah membuat errorHandler global untuk Todo List API. Tapi kalau diperhatikan lagi, error handler kita masih sangat sederhana: semua jenis error — entah itu data tidak ditemukan, input tidak valid, atau server benar-benar error — **diperlakukan sama** dan sering kali dikembalikan dengan status code yang kurang tepat.

Artikel ini akan membahas cara menentukan HTTP status code yang benar untuk tiap jenis error, dan cara membuat custom Error class agar error di seluruh aplikasi lebih terstruktur dan gampang diprediksi oleh tim frontend.

Artikel ini melanjutkan project Todo List API dari artikel pertama dan artikel kedua. Setelah ini, kita akan lanjut ke Panduan Autentikasi JWT dan Role-Based Access di Node.js.


1. Kenapa Status Code yang Tepat Itu Penting?

Analogi: bayangkan kamu mengirim surat izin ke wali kelas. Ada beberapa kemungkinan balasan: “Diterima”, “Ditolak karena tanda tangan orang tua belum ada”, “Ditolak karena kamu belum login sebagai siswa terdaftar”, atau “Surat kamu hilang, coba cari suratnya dulu.” Setiap balasan itu beda maknanya, dan wali kelas tidak mungkin membalas semuanya dengan satu kalimat generik seperti “Ada masalah.” HTTP status code bekerja seperti itu — setiap angka punya arti spesifik, supaya yang menerima response (frontend, aplikasi mobile, atau developer lain) langsung tahu jenis masalahnya tanpa harus menebak-nebak dari isi pesan errornya.

Kalau semua error dikembalikan dengan status 500, frontend jadi kesulitan membedakan “input kamu salah, coba perbaiki” dengan “server kami yang bermasalah, coba lagi nanti”. Padahal keduanya butuh penanganan yang beda di sisi frontend.

1.1 Tabel Status Code yang Wajib Dikuasai

Status Code

Nama

Kapan Dipakai

Analogi

200

OK

Request berhasil (GET, PUT, DELETE)

Surat izin diterima, semua beres

201

Created

Data baru berhasil dibuat (POST)

Pendaftaran ekskul baru berhasil dicatat

400

Bad Request

Input dari client tidak valid/lengkap

Formulir diisi tapi ada kolom wajib yang kosong

401

Unauthorized

Belum login / token tidak ada atau tidak valid

Belum tunjukkan kartu pelajar di gerbang

403

Forbidden

Sudah login, tapi tidak punya izin akses

Sudah tunjukkan kartu pelajar, tapi ruangan itu khusus guru

404

Not Found

Data atau endpoint yang diminta tidak ada

Mencari ruangan yang memang tidak ada di sekolah

409

Conflict

Data bentrok dengan data yang sudah ada (misal email sudah terdaftar)

Mendaftar ekskul yang jadwalnya bentrok dengan kelas lain

422

Unprocessable Entity

Format data benar, tapi isinya melanggar aturan bisnis

Formulir lengkap, tapi nomor teleponnya cuma 3 digit

500

Internal Server Error

Error tak terduga di server, bukan salah client

Mesin fotokopi sekolah tiba-tiba rusak, bukan salah siswa

Aturan sederhananya: 4xx = kesalahan ada di pihak client (data yang dikirim salah/kurang), sementara 5xx = kesalahan ada di pihak server (bug, database down, dll).


2. Membuat Custom Error Class

Analogi: selama ini, error di controller kita dilempar sebagai Error biasa — ibarat semua jenis surat pengaduan di sekolah (kehilangan barang, telat masuk, nilai salah input) ditulis di kertas polos yang sama tanpa kop surat atau kategori. Petugas TU jadi harus membaca isi suratnya satu-satu untuk tahu ini surat jenis apa. Custom Error class ibarat membuat kop surat khusus per kategori pengaduan, lengkap dengan “kode urusan” di pojok surat (status code), supaya petugas TU (error handler kita) langsung tahu cara memprosesnya tanpa perlu membaca detail dulu.

Buat file baru untuk custom Error class:

mkdir -p src/utils

src/utils/AppError.js

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true; // menandai ini error yang "diketahui/disengaja", bukan bug tak terduga

    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = AppError;

Dengan class ini, kita bisa melempar error yang sudah membawa status code-nya sendiri, misalnya throw new AppError("Todo not found", 404).


3. Update Global Error Handler

Sekarang kita upgrade errorHandler agar bisa membedakan beberapa jenis error secara otomatis:

  1. AppError — error yang sengaja kita lempar sendiri, sudah punya status code.

  2. Mongoose ValidationError — muncul saat data melanggar aturan schema (misal required tidak diisi).

  3. Mongoose CastError — muncul saat format ID yang dikirim tidak valid untuk MongoDB.

  4. Error tak terduga lainnya — dianggap error server (500), seperti bug atau database mati.

src/middlewares/errorHandler.middleware.js (update penuh)

function errorHandler(err, req, res, next) {
  console.error(err.stack);

  let statusCode = err.statusCode || 500;
  let message = err.message || "Internal Server Error";

  // Mongoose ValidationError — misal field "title" wajib diisi tapi kosong
  if (err.name === "ValidationError") {
    statusCode = 400;
    message = Object.values(err.errors)
      .map((item) => item.message)
      .join(", ");
  }

  // Mongoose CastError — misal ID yang dikirim bukan format ObjectId yang valid
  if (err.name === "CastError") {
    statusCode = 400;
    message = `Invalid value for field "${err.path}": ${err.value}`;
  }

  // Mongoose duplicate key error — misal field unique sudah dipakai data lain
  if (err.code === 11000) {
    statusCode = 409;
    const field = Object.keys(err.keyValue).join(", ");
    message = `Duplicate value for field: ${field}`;
  }

  res.status(statusCode).json({
    success: false,
    message,
    // Stack trace hanya ditampilkan saat development, jangan di production
    stack: process.env.NODE_ENV === "development" ? err.stack : undefined,
  });
}

module.exports = errorHandler;

3.1 Tambahkan Handler untuk Async Function Tanpa try-catch Berulang

Analogi: menulis try-catch di setiap function controller itu seperti setiap siswa harus bawa payung sendiri-sendiri padahal sekolah bisa saja punya satu atap besar yang melindungi semua orang sekaligus. catchAsync di bawah ini adalah “atap besar” itu — ia membungkus function controller, menangkap error apa pun yang terjadi di dalamnya, lalu otomatis meneruskannya ke next(error) tanpa kita perlu menulis try-catch berulang-ulang.

src/utils/catchAsync.js

function catchAsync(fn) {
  return function (req, res, next) {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

module.exports = catchAsync;

4. Refactor Controller dengan AppError dan catchAsync

Sekarang kita rapikan todo.controller.js menggunakan AppError dan catchAsync, sehingga tidak ada lagi try-catch manual dan status code ditentukan secara eksplisit dan konsisten.

src/controllers/todo.controller.js (update penuh)

const todoService = require("../services/todo.service");
const AppError = require("../utils/AppError");
const catchAsync = require("../utils/catchAsync");

const createTodo = catchAsync(async (req, res, next) => {
  const { title, description } = req.body;

  if (!title) {
    return next(new AppError("Title is required", 400));
  }

  const todo = await todoService.createTodo({ title, description });

  res.status(201).json({
    success: true,
    message: "Todo created successfully",
    data: todo,
  });
});

const getAllTodos = catchAsync(async (req, res, next) => {
  const todos = await todoService.getAllTodos();

  res.status(200).json({
    success: true,
    message: "Todos retrieved successfully",
    data: todos,
  });
});

const getTodoById = catchAsync(async (req, res, next) => {
  const { id } = req.params;
  const todo = await todoService.getTodoById(id);

  if (!todo) {
    return next(new AppError("Todo not found", 404));
  }

  res.status(200).json({
    success: true,
    message: "Todo retrieved successfully",
    data: todo,
  });
});

const updateTodo = catchAsync(async (req, res, next) => {
  const { id } = req.params;
  const { title, description, completed } = req.body;

  const updatedTodo = await todoService.updateTodo(id, {
    title,
    description,
    completed,
  });

  if (!updatedTodo) {
    return next(new AppError("Todo not found", 404));
  }

  res.status(200).json({
    success: true,
    message: "Todo updated successfully",
    data: updatedTodo,
  });
});

const deleteTodo = catchAsync(async (req, res, next) => {
  const { id } = req.params;
  const deletedTodo = await todoService.deleteTodo(id);

  if (!deletedTodo) {
    return next(new AppError("Todo not found", 404));
  }

  res.status(200).json({
    success: true,
    message: "Todo deleted successfully",
    data: deletedTodo,
  });
});

module.exports = {
  createTodo,
  getAllTodos,
  getTodoById,
  updateTodo,
  deleteTodo,
};

Perhatikan betapa lebih ringkasnya controller sekarang: tidak ada lagi try { ... } catch (error) { next(error) } yang berulang di setiap function, karena catchAsync sudah menanganinya secara otomatis.


5. Menangani Route yang Tidak Terdaftar dengan AppError Juga

Supaya konsisten, notFound middleware juga bisa kita ubah untuk melempar AppError, bukan langsung mengirim response — sehingga semua error, tanpa terkecuali, melewati errorHandler yang sama.

src/middlewares/notFound.middleware.js (update penuh)

const AppError = require("../utils/AppError");

function notFound(req, res, next) {
  next(new AppError(`Route ${req.method} ${req.originalUrl} not found`, 404));
}

module.exports = notFound;

6. Struktur Folder Setelah Artikel Ini

todo-api/
├── src/
│   ├── config/
│   │   └── db.js
│   ├── middlewares/
│   │   ├── logger.middleware.js
│   │   ├── notFound.middleware.js
│   │   └── errorHandler.middleware.js
│   ├── models/
│   │   └── todo.model.js
│   ├── services/
│   │   └── todo.service.js
│   ├── controllers/
│   │   └── todo.controller.js
│   ├── routes/
│   │   └── todo.routes.js
│   ├── utils/
│   │   ├── AppError.js
│   │   └── catchAsync.js
│   ├── app.js
│   └── server.js
├── .env
├── .env.example
├── .gitignore
└── package.json

7. Menguji Setiap Jenis Error

npm run dev

Tes 1 di terminal— Bad Request (400). Kirim POST tanpa title:

curl -X POST http://localhost:3000/api/todos \
  -H "Content-Type: application/json" \
  -d '{"description": "Tidak ada title"}'
{
  "success": false,
  "message": "Title is required"
}
test request title kosong

Tes 2 — Not Found (404). Ambil todo dengan ID yang formatnya valid tapi datanya tidak ada:

curl http://localhost:3000/api/todos/665f1c2e8b1e2a1a2c3d9999
{
  "success": false,
  "message": "Todo not found"
}

Tes 3 — Bad Request dari CastError (400). Ambil todo dengan format ID yang tidak valid:

curl http://localhost:3000/api/todos/id-ngasal
{
  "success": false,
  "message": "Invalid value for field \"_id\": id-ngasal"
}

Tes 4 — Route Not Found (404), tapi sekarang lewat AppError.

curl http://localhost:3000/api/unknown
{
  "success": false,
  "message": "Route GET /api/unknown not found"
}

8. Perbandingan Sebelum dan Sesudah

Aspek

Sebelum

Sesudah

Status code error

Sering asal 400/500, tidak konsisten

Ditentukan jelas per jenis error (400, 404, 409, 500, dst)

Penulisan controller

try-catch manual di tiap function

Dibungkus catchAsync, lebih ringkas

Error dari database

Dikembalikan sebagai pesan mentah Mongoose

Diterjemahkan jadi pesan & status code yang tepat

Konsistensi format error

Beda-beda tiap controller

Seragam lewat AppError + errorHandler


9. Kesimpulan

Dengan menerapkan AppError, catchAsync, dan error handler yang lebih pintar, Todo List API kita sekarang mengembalikan status code dan pesan error yang jelas dan konsisten untuk setiap jenis kesalahan. Ini sangat membantu tim frontend (atau dirimu sendiri di masa depan) untuk tahu persis bagaimana menangani tiap jenis response error tanpa harus menebak-nebak.

Sejauh ini, siapa pun bisa mengakses semua endpoint Todo List tanpa batasan — siapa saja bisa membuat, mengubah, bahkan menghapus todo milik orang lain. Di artikel selanjutnya, kita akan menutup celah ini dengan menambahkan sistem login dan otorisasi berbasis role.

Lanjutkan ke artikel berikutnya: Panduan Autentikasi JWT dan Role-Based Access di Node.js

Artikel Terkait

Panduan Membuat Animasi Animated on Scroll di Website Anda

Pelajari cara membuat animasi menarik pada scroll di website Anda dengan tutorial ini. Tampilkan konten dengan cara yang lebih interaktif!

Baca Artikel
Tutorial Membuat REST API dengan Laravel yang Mudah dan Praktis

Pelajari langkah demi langkah cara membuat REST API menggunakan Laravel dengan mudah. Cocok untuk pemula dan pengembang berpengalaman.

Baca Artikel
Cara Membuat Efek Parallax dengan HTML dan CSS

Pelajari cara mudah membuat efek parallax yang menakjubkan dengan HTML dan CSS dalam tutorial ini. Cocok untuk pemula!

Baca Artikel
Cara Menangani Error dan HTTP Status Code yang Benar | Silala Blog