Lo deploy API. Di local testing — mulus. Di production — 500 error karena user ngirim data yang gak lo expect. Lo gak bisa nebak semua input user. Tapi lo bisa bikin error handling yang bikin API lo graceful.
Lo baru nulis API — user endpoint, CRUD biasa. Di Postman, semua request lancar: 201 Created, 200 OK. Lo push ke production. Seneng. Besok pagi — lo buka error log. 400 Bad Request. 500 Internal Server Error. User ngirim string di field yang harusnya angka. User ngirim field yang gak lo define. User ngirim request body kosong.
Lo fix satu error, muncul error lain. Lo debug, fix, deploy. Sehari kemudian — error baru lagi. Lo capek.
Yang masalah bukan lo yang gak jago nulis kode. Yang masalah: lo gak nulis error handling yang defensive dari awal. Lo nulis kode buat happy path — waktu semua input bener. Di production, input yang gak bener itu yang paling banyak dateng.
Artikel ini: panduan error handling untuk REST API yang bikin lo gak perlu debug satu-satu di production. Lo handle semua error di satu tempat, dengan response yang konsisten dan informatif — tanpa kebocoran data internal.
Kesalahan #1: Lo Nge-throw Error Mentah ke User
Ini kesalahan paling umum. Lo pake framework (Express, Laravel, FastAPI) dan lo lempar error mentah dari framework:
// Express.js - jangan lakukan ini
app.post('/users', (req, res) => {
const { name, email } = req.body;
const result = await db.query('INSERT INTO users...', [name, email]);
res.json({ id: result.insertId });
});
Coba kirim request dengan req.body = {}. Lo dapet error mentah dari database: Column 'name' cannot be null. Sekarang user lo tau: (1) lo pake database apa, (2) ada kolom ‘name’ di table users, (3) error-nya dari query SQL. Lo udah leak informasi internal ke user lo. Yang lebih buruk: ini bisa jadi vector SQL injection kalo lo gak hati-hati.
Solusi 1: Validasi di Layer Paling Awal
Lo harus ngevalidasi input sebelum sampai ke logic bisnis. Paling awal, paling gampang.
// Express.js dengan Joi
const Joi = require('joi');
const createUserSchema = Joi.object({
name: Joi.string().min(2).max(100).required(),
email: Joi.string().email().required(),
age: Joi.number().integer().min(17).max(120).optional(),
});
app.post('/users', (req, res) => {
const { error, value } = createUserSchema.validate(req.body, { abortEarly: false });
if (error) {
return res.status(400).json({
error: 'VALIDATION_ERROR',
message: 'Ada field yang gak valid.',
details: error.details.map(d => ({
field: d.path.join('.'),
message: d.message,
})),
});
}
// Sekarang value udah di-guarantee valid
const result = await db.query('INSERT INTO users...', [value.name, value.email]);
res.status(201).json({ id: result.insertId });
});
Perbedaannya: sekarang lo dapet response yang informatif tapi gak leak informasi internal:
{
"error": "VALIDATION_ERROR",
"message": "Ada field yang gak valid.",
"details": [
{ "field": "email", "message": "\"email\" must be a valid email" }
]
}
Response ini: (1) gak leak nama kolom database, (2) kasih tau ke user mana yang salah, (3) konsisten untuk semua endpoint. User lo bisa fix input mereka tanpa harus tanya ke lo.
Solusi 2: Global Error Handler
Validasi di layer awal cover input validation. Tapi ada error lain yang gak bisa lo cegah: database error, external API timeout, file system error. Yang gak boleh lo lakuin: nge-throw error ini ke user.
// Express.js - global error handler di paling akhir
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
// Database error
if (err.code === 'ER_DUP_ENTRY') {
return res.status(409).json({
error: 'CONFLICT',
message: 'Data yang lo masukin udah ada.',
});
}
// Database connection error
if (err.code === 'ECONNREFUSED') {
return res.status(503).json({
error: 'SERVICE_UNAVAILABLE',
message: 'Service lagi down. Coba beberapa saat lagi.',
});
}
// Default: 500
res.status(500).json({
error: 'INTERNAL_ERROR',
message: 'Ada kesalahan internal. Tim kami udah dikirim notifikasi.',
});
});
Prinsip: error apa pun yang sampai ke global handler = error yang gak lo handle. Lo log error mentah buat tim lo, dan kirim response generik ke user. User gak perlu tau kenapa error-nya yang terjadi — yang lo perlu kasih ke mereka: angka HTTP status yang benar, pesan yang bisa mereka pahami, dan path untuk resolve masalah.
Kesalahan #2: Lo Nge-return Status Code yang Salah
Kesalahan umum yang bikin API lo confusing:
- Request dengan field yang gak valid → lo kirim 500. Seharusnya: 400 Bad Request.
- User coba akses resource yang gak ada → lo kirim 500. Seharusnya: 404 Not Found.
- User coba akses resource yang bukan miliknya → lo kirim 500. Seharusnya: 403 Forbidden.
- Database down → lo kirim 500. Yang ini emang bener, tapi harusnya juga 503 kalau lo tau service external down.
Kenapa ini penting? Karena HTTP status code itu bahasa universal antara API dan client. Frontend lo, mobile app lo, bahkan API client lain — mereka pake status code buat tau apa yang harus dilakuin. 4xx = perbaiki request. 5xx = coba lagi nanti. Kalau semua error lo kirim 500, client lo gak bisa nge-handle error dengan benar.
Referensi Cepat: Status Code yang Lo Butuh
- 200 OK — Request berhasil, data dikirim.
- 201 Created — Resource baru berhasil dibuat.
- 204 No Content — Request berhasil, tapi gak ada data yang dikirim (biasanya hapus/update).
- 400 Bad Request — Input user gak valid. Lo tau kenapa, user juga harus tau.
- 401 Unauthorized — User belum login / token expired.
- 403 Forbidden — User udah login, tapi gak punya akses ke resource ini.
- 404 Not Found — Resource gak ditemukan.
- 409 Conflict — Data yang diminta udah ada (misal duplicate entry).
- 422 Unprocessable — Format request valid, tapi isi gak bisa diproses.
- 429 Too Many Requests — Rate limit exceeded.
- 500 Internal Error — Lo gak tau kenapa error-nya, tapi pasti di sisi lo.
- 503 Service Unavailable — Service external (database, API lain) down.
Solusi 3: Standard Error Response Format
Setiap error response lo harus punya format yang konsisten. Biar client lo gak harus nulis parser berbeda tiap endpoint. Minimal:
{
"error": "VALIDATION_ERROR",
"message": "Ada field yang gak valid.",
"details": [
{ "field": "email", "message": "\"email\" must be a valid email" }
],
"timestamp": "2026-06-21T14:30:00.000Z",
"requestId": "req_abc123"
}
Kenapa requestId penting? Karena kalau user lo laporkan error ke lo, mereka bisa kirim requestId-nya. Lo langsung tau error mana yang mereka alami tanpa harus minta log. Ini hemat waktu semua pihak.
Solusi 4: Custom App Error Class
Lo bisa bikin class error sendiri yang bisa di-throw dari mana aja di codebase lo:
// app/error.js
class AppError extends Error {
constructor(statusCode, errorCode, message, details = null) {
super(message);
this.statusCode = statusCode;
this.errorCode = errorCode;
this.details = details;
}
}
// Di mana aja di codebase lo
throw new AppError(400, 'VALIDATION_ERROR', 'Field email wajib diisi.', [
{ field: 'email', message: '"email" is required' }
]);
throw new AppError(404, 'NOT_FOUND', 'User dengan ID ini gak ditemukan.');
throw new AppError(409, 'CONFLICT', 'Email yang lo masukin udah terdaftar.');
Sekarang error handling lo: konsisten, centralized, dan gampang di-maintain. Lo juga bisa extend AppError untuk logging otomatis ke Sentry/Datadog — setiap error otomatis ke monitoring tanpa lo nulis logging manual.
Kesalahan #3: Lo Nge-log Error tapi Gak Action
Banyak developer nge-log error ke console tapi gak ada sistem yang ngasih tau mereka kalo ada error. Error log yang gak dibaca itu sama aja gak ada log.
Minimal: pake error reporting service. Sentry (gratis untuk 5rb error per bulan) atau Bugsnag atau Rollbar. Setup-nya 10 menit. Begitu ada error 500, lo dapet email + dashboard. Lo gak perlu nunggu user complain.
// Sentry setup - Express.js
const Sentry = require('@sentry/node');
Sentry.init({ dsn: 'your-dsn-here' });
// Sentry middleware - taruh di ATAS error handler
app.use(Sentry.Handlers.requestHandler());
// Global error handler - Sentry capture
app.use((err, req, res, next) => {
Sentry.captureException(err);
// ... sisa error handling
});
Checklist: Error Handling yang Lo Butuh Sebelum Deploy
- Validasi input di layer paling awal — sebelum sampai ke logic bisnis.
- Global error handler di layer paling akhir — catch semua error yang lolos.
- Status code yang benar — 4xx untuk user error, 5xx untuk server error.
- Standard response format — error, message, details, requestId.
- Error class — throw dari mana aja, handle di satu tempat.
- Error reporting — error 500 langsung ke email/monitoring, bukan cuma ke console.
- Gak leak informasi internal — user gak boleh tau stack trace, nama kolom database, atau nama file server lo.
7 langkah ini cover 90% error handling yang lo butuh. Sisanya? Itu masalah spesifik domain lo — rate limiting per role, retry logic untuk external API, circuit breaker untuk service dependency. Tapi 7 di atas udah jadi fondasi yang bikin API lo gak bikin lo capek debugging di production jam 3 pagi.