Belajar Backend Developer - Logging & Error Handling
Episode 11 of 28

Belajar Backend Developer - Logging & Error Handling

Menjadikan produksi tidak buta: structured logging yang bisa dicari dan dianalisis, taksonomi error yang konsisten, error handler terpusat, dan tracing dasar untuk melacak satu request melintasi layanan

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

Setelah di episode 10 alur kalian menjadi asinkron dan tersebar di worker, muncul masalah baru: bagaimana tahu apa yang terjadi di produksi? Di tahap awal, kalian mungkin men-debug dengan console.log — tetapi di produksi, log itu tidak terstruktur, tersebar di banyak proses, dan tidak bisa dicari. Inilah saat observability masuk.

Mengapa logging & error handling penting? Karena bug produksi hampir selalu menyerang saat kita tidak melihat langsung. Log yang baik mengubah "entah kenapa rusak" menjadi "di endpoint ini, untuk user ini, terjadi error ini pada langkah itu". Episode ini membangun structured logging, taksonomi error, error handler terpusat, dan tracing dasar.

Masalah console.log

JSconsole.log - tidak terstruktur
console.log("order created", orderId, userId, "total:", total)

Log ini sulit dicari, sulit di-filter, dan tidak konsisten formatnya. Di produksi dengan ribuan request, log seperti ini menjadi dinding teks yang tidak berguna. Solusinya: structured logging — log sebagai data (JSON), bukan sebagai kalimat.

Structured Logging

Structured logging mencatat field berpasangan sehingga bisa di-query: filter per orderId, hitung per endpoint, alarm per level.

Pino - structured logging
import pino from "pino"
 
export const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  base: { service: "shop-api" },
})
 
logger.info({ orderId: 87, userId: 5 }, "order_created")
 
logger.error(
  { err, orderId: 87, step: "payment" },
  "payment_failed",
)

Output-nya adalah JSON yang bisa dianalisis:

Output log terstruktur
{
  "level": 30,
  "time": 1784149200000,
  "service": "shop-api",
  "msg": "order_created",
  "orderId": 87,
  "userId": 5
}

Aturan praktis:

  • Jangan log data sensitif — password, token, nomor kartu (episode 19 soal privacy).
  • Pakai message code (order_created, payment_failed) — stabil dan bisa dipakai untuk alerting, bukan kalimat yang berubah-ubah.
  • Tambahkan context: userId, orderId, requestId agar satu kejadian bisa dilacak.

Taksonomi Error

Semua error sebaiknya dikelompokkan agar tahu mana yang perlu alarm dan mana yang normal:

KelasContohTindakan
Client error400, 401, 404, 409Log warn, bukan error
Expected failurePayment ditolakLog warn + metric
Unexpected errorException tak dikenalLog error + alert + stack trace
FatalOut of memoryLog fatal + alert segera

Kuncinya: jangan beri error untuk sesuatu yang sebenarnya normal. Alarm yang terlalu banyak membuat tim mati rasa dan mengabaikan yang benar-benar penting.

Error Handler Terpusat

Kode kalian harus melempar error ke satu titik yang mencatat dan merespons dengan konsisten — bukan menangani error di tiap handler secara berbeda.

classes/http-error.ts
export class HttpError extends Error {
  constructor(
    public status: number,
    public code: string,
    public message: string,
    public details?: unknown,
  ) {
    super(message)
  }
}
middleware/error-handler.ts - terpusat
import type { NextFunction, Request, Response } from "express"
import { HttpError } from "../classes/http-error.js"
import { logger } from "./logger.js"
 
export function errorHandler(
  err: unknown,
  req: Request,
  res: Response,
  _next: NextFunction,
) {
  if (err instanceof HttpError) {
    logger.warn(
      { code: err.code, status: err.status, path: req.path },
      "client_error",
    )
    return res.status(err.status).json({
      error: err.code,
      message: err.message,
      details: err.details,
    })
  }
 
  logger.error({ err, path: req.path }, "unhandled_error")
  res.status(500).json({
    error: "INTERNAL_ERROR",
    message: "Terjadi kesalahan di server",
  })
}

Handler ini memisahkan dua kelas: error yang diketahui (HttpError → respons bersih + log warn) dan yang tidak diketahui (log error + 500 generik). Jangan pernah membocorkan stack trace ke client — itu intel untuk penyerang.

Note

Pastikan error handler dipasang setelah semua route dan menangkap error async. Framework modern (Express 5, Fastify) meneruskan rejection Promise otomatis ke error handler; framework lama wajib memakai wrapper. Uji dengan sengaja melempar error di satu endpoint untuk memastikan alur ini bekerja.

Tracing Dasar: Melacak Satu Request

Log terstruktur mencatat banyak hal, tapi saat satu request melewati banyak layanan (API → queue → worker → database), bagaimana menyatukan semua log itu? Jawabannya tracing dengan traceId yang dibawa dari awal hingga akhir.

Middleware tracing
import { randomUUID } from "node:crypto"
 
app.use((req, res, next) => {
  const traceId = req.headers["x-trace-id"] ?? randomUUID()
  req.traceId = traceId
  res.setHeader("x-trace-id", traceId)
  next()
})

Setiap log di dalam satu request membawa traceId yang sama, sehingga saat ada error bisa dilakukan:

Cari semua log satu request
curl -s http://localhost:3000/v1/orders/87 | grep -o '"traceId":"[^"]*"' | head -1
# lalu filter log dengan traceId tersebut

Untuk produksi yang lebih serius, pakai OpenTelemetry — standar terbuka yang mengirim trace ke Jaeger/Tempo, menggabungkan log, metrik, dan trace dalam satu dasbor. Kita sentuh ini di episode 25.

Logging di Produksi

Struktur File dan Rotasi

Jangan menulis log ke file manual di dalam aplikasi — biarkan log mengalir ke stdout, dan prosesor log eksternal (systemd, Docker, atau agent logging) yang menangani file dan rotasi.

docker-compose - log ke stdout, driver Docker
services:
  api:
    image: shop-api:latest
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "5"

Agregasi dan Alerting

Log terstruktur dikirim ke sistem agregasi (ELK, Loki, CloudWatch) dan dijadikan alarm:

  • Alarm: error rate per endpoint > ambang.
  • Alarm: payment_failed berulang dalam 5 menit.
  • Alarm: panjang queue (episode 10) bertambah terus.

Tip

Mulailah sesederhana mungkin: pino untuk structured log ke stdout, x-trace-id untuk menghubungkan log, dan sebuah dasbor sederhana. Jangan langsung pasang platform observability penuh — log terstruktur yang konsisten lebih berharga daripada tooling yang canggih.

Common Pitfalls

Log Sensitif

Mencatat email, token, atau body penuh request = kebocoran data diam-diam. Redact field sensitif di serializer log (episode 19).

Semua Error = error

Client error yang normal di-log sebagai error membuat alarm banjir. warn untuk yang diketahui, error untuk yang tidak terduga.

Tidak Ada Context

logger.error("gagal") tanpa orderId/traceId tidak bisa diinvestigasi. Selalu lampirkan konteks yang cukup untuk reproduksi.

Stack Trace Bocor ke Client

Response 500 harus generik; detail error hanya di log server.

Penutup

Episode 11 menjadikan sistem terlihat: structured logging dengan pino, taksonomi error yang mengelompokkan berdasarkan urgensi, error handler terpusat, dan tracing dengan traceId untuk menyatukan perjalanan satu request.

Inti yang harus dibawa pulang:

  • Ganti console.log dengan structured logging (JSON) yang bisa di-query.
  • Taksonomi error: client error → warn; tak terduga → error + alarm.
  • Error handler terpusat menyeragamkan respons dan pencatatan.
  • traceId menyatukan log satu request lintas layanan.
  • Jangan pernah log data sensitif atau bocorkan stack trace ke client.

Di episode 12 selanjutnya kita akan membedah arsitektur: microservices & modular monolith — perbandingan jujur kedua pendekatan dan tren 2026 yang pragmatis. Sampai jumpa di episode 12!

Belajar Backend Developer - Logging & Error Handling | Belajar Backend