Fail Fast #

Ada dua cara sistem bereaksi terhadap kondisi yang tidak valid. Cara pertama: lanjutkan saja, sembunyikan errornya, coba tangani sebisanya, dan berharap tidak ada yang rusak. Cara kedua: hentikan segera, laporkan dengan jelas apa yang salah, dan jangan biarkan state yang tidak valid menyebar lebih jauh. Fail Fast adalah cara kedua — dan ia bukan tentang membuat sistem yang mudah crash, tapi tentang membuat sistem yang jujur tentang kondisinya sendiri. Sistem yang gagal cepat dengan pesan yang jelas jauh lebih mudah di-debug dan diperbaiki daripada sistem yang gagal lambat dengan error misterius di tempat yang jauh dari sumber masalahnya. Prinsip Fail Fast menyatakan: deteksi kondisi error sesegera mungkin, laporkan dengan konteks yang cukup, dan jangan lanjutkan eksekusi dengan state yang tidak valid. Artikel ini membahas empat level penerapan Fail Fast — dari validasi input hingga arsitektur sistem — perbedaannya dengan Fail Safe, kapan panic vs error yang tepat di Go, dan kapan Fail Fast justru berbahaya dan perlu dikombinasikan dengan strategi recovery.

Mengapa Fail Fast? #

Bayangkan sebuah fungsi yang menerima konfigurasi database yang kosong, lalu melanjutkan eksekusi dan mencoba membuka koneksi — yang tentu saja gagal. Error yang muncul adalah "connection refused to :0" bukan "database DSN is required". Engineer yang debugnya harus mundur dari error connection ke kode yang seharusnya memvalidasi config — membuang waktu yang tidak perlu.

Tanpa Fail Fast:                    Dengan Fail Fast:
──────────────────────────────      ──────────────────────────────────
Config kosong dibaca                Config kosong dibaca
  ↓                                   ↓
Diteruskan ke DB connector          Validasi: DSN kosong → STOP
  ↓                                   ↓
Coba connect ke ":0"                Error langsung:
  ↓                                   "database DSN is required"
"connection refused to :0"
  ↓                                 Engineer langsung tahu apa masalahnya
  ↓                                 dan di mana harus memperbaikinya
Request berikutnya: panic
di tengah handler
  ↓
"nil pointer dereference"

Engineer harus mundur dari
nil pointer → cari di mana db
diinisialisasi → cari config
→ barulah tahu masalahnya

Jarak antara sumber error dan tempat error terdeteksi adalah biaya debugging yang tidak perlu. Fail Fast meminimalkan jarak itu.

flowchart LR
    subgraph SLOW["Tanpa Fail Fast — error jauh dari sumbernya"]
        direction TB
        S1["Config kosong"] --> S2["Diteruskan"] --> S3["Koneksi gagal"] --> S4["Nil pointer"] --> S5["💥 Panic di handler\n(jauh dari sumber)"]
    end

    subgraph FAST["Dengan Fail Fast — error dekat dengan sumbernya"]
        direction TB
        F1["Config kosong"] --> F2["💥 Validasi gagal\n(dekat dengan sumber)"]
    end

    style S5 fill:#D9534F,color:#fff
    style F2 fill:#5CB85C,color:#fff

Level 1 — Fail Fast di Input Validation #

Level pertama dan paling dasar: validasi semua input di titik masuk — bukan di tengah-tengah pemrosesan.

// ANTI-PATTERN: validasi tersebar dan terlambat
func TransferFunds(fromID, toID string, amount float64) error {
    // Tidak ada validasi di awal
    from, err := repo.FindAccount(fromID)
    if err != nil {
        return err // error terlambat — sudah query DB untuk ID yang mungkin kosong
    }

    to, err := repo.FindAccount(toID)
    if err != nil {
        return err
    }

    if from.Balance < amount { // validasi bisnis baru di sini
        return errors.New("insufficient balance")
    }

    if amount <= 0 { // validasi seharusnya paling pertama, malah paling akhir
        return errors.New("amount must be positive")
    }

    return repo.Transfer(from, to, amount)
}

// BENAR: semua validasi di awal — fail fast sebelum melakukan operasi apapun
func TransferFunds(ctx context.Context, fromID, toID string, amount float64) error {
    // === Fail Fast: validasi semua input sebelum melakukan apapun ===
    if fromID == "" {
        return errors.New("fromID is required")
    }
    if toID == "" {
        return errors.New("toID is required")
    }
    if fromID == toID {
        return errors.New("cannot transfer to the same account")
    }
    if amount <= 0 {
        return fmt.Errorf("amount must be positive, got %.2f", amount)
    }
    if amount > MaxSingleTransferAmount {
        return fmt.Errorf("amount %.2f exceeds maximum single transfer limit %.2f",
            amount, MaxSingleTransferAmount)
    }

    // Setelah semua validasi lulus, baru mulai operasi
    from, err := repo.FindAccount(ctx, fromID)
    if err != nil {
        return fmt.Errorf("transferFunds: find source account: %w", err)
    }

    if from.Balance < amount {
        return fmt.Errorf("insufficient balance: have %.2f, need %.2f",
            from.Balance, amount)
    }

    to, err := repo.FindAccount(ctx, toID)
    if err != nil {
        return fmt.Errorf("transferFunds: find destination account: %w", err)
    }

    return repo.Transfer(ctx, from, to, amount)
}

Fail Fast di input validation berarti semua guard clause ada di atas, sebelum operasi apapun dimulai. Ini bukan hanya KISS (guard clause menggantikan nested condition) — ini juga Fail Fast karena error dilaporkan sebelum ada side effect.


Level 2 — Fail Fast di Startup #

Startup adalah kesempatan terbaik untuk Fail Fast. Lebih baik aplikasi tidak bisa start dengan pesan error yang jelas daripada start dengan konfigurasi yang salah lalu crash secara misterius di production saat pertama kali ada request yang menyentuh fitur tersebut.

// ANTI-PATTERN: aplikasi start meski config tidak lengkap
func main() {
    cfg := &Config{
        DBDSN:  os.Getenv("DATABASE_URL"), // mungkin kosong
        Port:   os.Getenv("PORT"),         // mungkin kosong
        APIKey: os.Getenv("API_KEY"),      // mungkin kosong
    }
    // Tidak ada validasi — aplikasi start, crash nanti
    startServer(cfg)
}

// BENAR: validasi semua yang diperlukan sebelum aplikasi dianggap siap
func main() {
    cfg, err := loadAndValidateConfig()
    if err != nil {
        // Gunakan log.Fatal bukan panic — pesan lebih bersih di log
        log.Fatalf("startup failed: invalid configuration:\n%v", err)
    }

    // Cek konektivitas ke semua dependency kritikal sebelum mulai serve
    if err := checkDependencies(cfg); err != nil {
        log.Fatalf("startup failed: dependency check:\n%v", err)
    }

    log.Printf("all checks passed, starting server on :%d", cfg.Port)
    startServer(cfg)
}

func loadAndValidateConfig() (*Config, error) {
    cfg := &Config{
        DBDSN:       os.Getenv("DATABASE_URL"),
        Port:        getEnvInt("PORT", 8080),
        APIKey:      os.Getenv("PAYMENT_API_KEY"),
        JWTSecret:   os.Getenv("JWT_SECRET"),
        KafkaBroker: os.Getenv("KAFKA_BROKER"),
    }

    var errs []string

    if cfg.DBDSN == "" {
        errs = append(errs, "DATABASE_URL is required")
    }
    if cfg.Port <= 0 || cfg.Port > 65535 {
        errs = append(errs, fmt.Sprintf("PORT must be between 1-65535, got %d", cfg.Port))
    }
    if cfg.APIKey == "" {
        errs = append(errs, "PAYMENT_API_KEY is required")
    }
    if len(cfg.JWTSecret) < 32 {
        errs = append(errs, "JWT_SECRET must be at least 32 characters")
    }
    if cfg.KafkaBroker == "" {
        errs = append(errs, "KAFKA_BROKER is required")
    }

    if len(errs) > 0 {
        return nil, fmt.Errorf("missing or invalid configuration:\n  - %s",
            strings.Join(errs, "\n  - "))
    }
    return cfg, nil
}

func checkDependencies(cfg *Config) error {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Cek database
    db, err := sql.Open("postgres", cfg.DBDSN)
    if err != nil {
        return fmt.Errorf("database: open connection: %w", err)
    }
    defer db.Close()
    if err := db.PingContext(ctx); err != nil {
        return fmt.Errorf("database: ping failed (is the database running?): %w", err)
    }
    log.Println("✓ database connection ok")

    // Cek schema migration sudah up to date
    if err := checkMigrationVersion(ctx, db); err != nil {
        return fmt.Errorf("database: migration check: %w", err)
    }
    log.Println("✓ database schema up to date")

    return nil
}

Startup validation yang komprehensif memberikan manfaat ganda: mencegah crash misterius di runtime, dan mendokumentasikan semua dependency yang diperlukan aplikasi secara eksplisit di satu tempat.


Level 3 — Fail Fast dengan panic vs error di Go #

Go membedakan dua mekanisme untuk melaporkan kondisi yang tidak diharapkan: error untuk kondisi yang diharapkan bisa terjadi dan harus ditangani caller, dan panic untuk kondisi yang seharusnya tidak pernah terjadi — programmer error atau invariant yang dilanggar.

Fail Fast dengan panic yang tepat adalah alat yang valid di Go, tapi penggunaannya harus terbatas dan terdokumentasi dengan baik.

// KAPAN MENGGUNAKAN error (kondisi yang bisa terjadi dan caller harus tangani):
func FindUser(ctx context.Context, id string) (*User, error) {
    if id == "" {
        return nil, errors.New("id is required") // caller harus handle ini
    }
    user, err := db.QueryUser(ctx, id)
    if errors.Is(err, sql.ErrNoRows) {
        return nil, ErrUserNotFound // kondisi yang valid dan diharapkan
    }
    if err != nil {
        return nil, fmt.Errorf("findUser: %w", err)
    }
    return user, nil
}

// KAPAN MENGGUNAKAN panic (programmer error — seharusnya tidak pernah terjadi):

// 1. Dependency yang di-inject adalah nil — ini adalah bug, bukan kondisi runtime
func NewOrderService(repo OrderRepository, notifier Notifier) *OrderService {
    if repo == nil {
        panic("NewOrderService: repo must not be nil") // programmer error
    }
    if notifier == nil {
        panic("NewOrderService: notifier must not be nil") // programmer error
    }
    return &OrderService{repo: repo, notifier: notifier}
}

// 2. Switch/select yang seharusnya exhaustive
func statusToHTTP(s order.Status) int {
    switch s {
    case order.StatusPending:
        return http.StatusAccepted
    case order.StatusConfirmed:
        return http.StatusOK
    case order.StatusCancelled:
        return http.StatusGone
    default:
        // Jika ada Status baru ditambahkan tapi switch ini tidak diupdate,
        // panic lebih baik daripada mengembalikan nilai yang salah secara silent
        panic(fmt.Sprintf("statusToHTTP: unhandled status %q", s))
    }
}

// 3. Inisialisasi package-level yang harus berhasil (regex, template)
var (
    // MustCompile adalah idiom Go standar untuk Fail Fast saat init
    emailRegex = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
    // Jika regex tidak valid, tidak ada gunanya aplikasi berjalan
)

// Membuat fungsi "Must" sendiri untuk kasus serupa:
func mustParseTemplate(name, tmpl string) *template.Template {
    t, err := template.New(name).Parse(tmpl)
    if err != nil {
        panic(fmt.Sprintf("mustParseTemplate %q: %v", name, err))
    }
    return t
}

var welcomeTemplate = mustParseTemplate("welcome", `
    <h1>Welcome, {{.Name}}!</h1>
    <p>Your account has been created.</p>
`)

Panduan kapan panic vs error yang tepat:

GUNAKAN error KETIKA:
  ✓ Kondisi bisa terjadi di runtime karena input atau state eksternal
  ✓ Caller bisa dan harus mengambil keputusan berdasarkan error ini
  ✓ Ada cara yang masuk akal untuk menangani kondisi ini
  Contoh: file not found, network timeout, record not found, validation failed

GUNAKAN panic KETIKA:
  ✓ Ini adalah programmer error — seharusnya tidak pernah terjadi
  ✓ Mengembalikan error akan menyembunyikan bug yang serius
  ✓ Kondisi ini menandakan invariant sistem yang dilanggar
  Contoh: nil dependency di-inject, switch case yang tidak exhaustive,
          inisialisasi yang seharusnya selalu berhasil

JANGAN panic KETIKA:
  ✗ Kondisi bisa terjadi karena input dari user atau sistem eksternal
  ✗ Library atau package yang bisa dipakai oleh code lain
    (panic dari library tidak bisa dicegah oleh caller)
  ✗ Hanya karena "ini seharusnya tidak terjadi" tanpa keyakinan penuh

Level 4 — Fail Fast di Arsitektur Sistem #

Di level sistem, Fail Fast berarti mendeteksi dan merespons kondisi tidak sehat secepat mungkin — sebelum error menyebar ke komponen lain atau ke user.

Fail Fast di API layer dengan error yang informatif:

// ANTI-PATTERN: swallow error dan kembalikan response generik
func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    var req CreateOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "bad request", 400) // tidak informatif
        return
    }

    result, err := h.service.CreateOrder(r.Context(), req)
    if err != nil {
        http.Error(w, "error", 500) // ← tidak ada informasi untuk debugging
        return
    }

    json.NewEncoder(w).Encode(result)
}

// BENAR: error yang cukup informatif untuk debugging tanpa expose internal detail
type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    TraceID string `json:"trace_id,omitempty"` // untuk korelasi di log
}

func (h *Handler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    traceID := middleware.TraceIDFromContext(r.Context())

    var req CreateOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeAPIError(w, http.StatusBadRequest, APIError{
            Code:    "INVALID_REQUEST_BODY",
            Message: "request body is not valid JSON",
            TraceID: traceID,
        })
        return
    }

    result, err := h.service.CreateOrder(r.Context(), req)
    if err != nil {
        // Log detail internal untuk debugging
        slog.Error("create order failed",
            "trace_id", traceID,
            "user_id", req.UserID,
            "error", err,
        )

        // Terjemahkan ke error yang tepat untuk client
        switch {
        case errors.Is(err, order.ErrUserNotActive):
            writeAPIError(w, http.StatusForbidden, APIError{
                Code:    "USER_NOT_ACTIVE",
                Message: "your account is not active",
                TraceID: traceID,
            })
        case errors.Is(err, order.ErrInsufficientStock):
            writeAPIError(w, http.StatusConflict, APIError{
                Code:    "INSUFFICIENT_STOCK",
                Message: "one or more items are out of stock",
                TraceID: traceID,
            })
        default:
            writeAPIError(w, http.StatusInternalServerError, APIError{
                Code:    "INTERNAL_ERROR",
                Message: "an unexpected error occurred",
                TraceID: traceID, // trace ID agar support bisa korelasi dengan log
            })
        }
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(result)
}

Fail Fast di domain model dengan invariant enforcement:

// Domain model yang enforce invariant-nya sendiri
// bukan mengandalkan caller untuk selalu ingat memvalidasi

type Money struct {
    amount   int64  // dalam sen, tidak bisa negatif
    currency string // harus ISO 4217
}

// Constructor yang Fail Fast — tidak ada cara membuat Money yang tidak valid
func NewMoney(amount int64, currency string) (Money, error) {
    if amount < 0 {
        return Money{}, fmt.Errorf("money amount cannot be negative: %d", amount)
    }
    if !isValidCurrency(currency) {
        return Money{}, fmt.Errorf("invalid currency code: %q", currency)
    }
    return Money{amount: amount, currency: currency}, nil
}

// Atau versi Must untuk konteks yang dijamin valid
func MustNewMoney(amount int64, currency string) Money {
    m, err := NewMoney(amount, currency)
    if err != nil {
        panic(fmt.Sprintf("MustNewMoney: %v", err))
    }
    return m
}

func (m Money) Add(other Money) (Money, error) {
    if m.currency != other.currency {
        return Money{}, fmt.Errorf("cannot add %s and %s", m.currency, other.currency)
    }
    return Money{amount: m.amount + other.amount, currency: m.currency}, nil
}

// Struct ini tidak bisa diinisialisasi dengan nilai tidak valid
// karena field-nya private dan hanya bisa dibuat lewat NewMoney

Fail Fast vs Fail Safe #

Fail Fast sering dikontraskan dengan Fail Safe — dan keduanya bukan pilihan yang saling eksklusif. Yang tepat bergantung pada konteks dan konsekuensi kegagalan.

FAIL FAST:
  Prinsip : Hentikan segera saat kondisi tidak valid terdeteksi
  Tujuan  : Cegah state yang tidak valid menyebar; debug lebih mudah
  Cocok untuk: Startup validation, developer-facing errors, programmer errors,
               kondisi yang seharusnya tidak terjadi sama sekali
  Contoh  : Config tidak lengkap → tolak start
            Nil dependency → panic
            Switch case tidak exhaustive → panic

FAIL SAFE:
  Prinsip : Ketika gagal, jatuh ke mode yang aman atau terdegradasi
  Tujuan  : Jaga availability meski ada komponen yang tidak berfungsi
  Cocok untuk: User-facing features, degraded mode yang masih valuable,
               kondisi yang wajar terjadi di production
  Contoh  : Recommendation service down → tampilkan produk populer sebagai default
            Cache miss → fallback ke database
            Payment gateway A down → coba payment gateway B

KOMBINASI KEDUANYA (paling umum):
  Fail Fast untuk kondisi yang tidak boleh terjadi (startup, programmer errors)
  Fail Safe untuk kondisi yang bisa terjadi di runtime (dependency down, timeout)
flowchart TD
    ERR["Kondisi Error\nTerdeteksi"]

    Q1{"Apakah ini seharusnya\ntidak pernah terjadi?\n(programmer error /\ninvariant dilanggar)"}
    Q2{"Ada fallback yang\nmemberikan nilai nyata\nbagi user?"}
    Q3{"Apakah ini di\nstartup / inisialisasi?"}

    FF1["Fail Fast\npanic dengan pesan jelas"]
    FF2["Fail Fast\nlog.Fatal di startup"]
    FS["Fail Safe\nfallback / degraded mode"]
    ERR2["Return error\nke caller dengan konteks"]

    ERR --> Q1
    Q1 -->|"Ya"| Q3
    Q1 -->|"Tidak"| Q2
    Q3 -->|"Ya"| FF2
    Q3 -->|"Tidak"| FF1
    Q2 -->|"Ya"| FS
    Q2 -->|"Tidak"| ERR2

    style FF1 fill:#D9534F,color:#fff
    style FF2 fill:#D9534F,color:#fff
    style FS fill:#4C9BE8,color:#fff
    style ERR2 fill:#5CB85C,color:#fff

Fail Fast di Dart/Flutter #

Di Dart, Fail Fast paling sering muncul dalam dua konteks: assertion untuk invariant developer, dan early return di business logic.

// ANTI-PATTERN: constructor yang menerima state tidak valid secara diam-diam
class Order {
    final String id;
    final String userId;
    final List<OrderItem> items;
    final double total;

    Order({
        required this.id,
        required this.userId,
        required this.items,
        required this.total,
        // Tidak ada validasi — bisa dibuat dengan total negatif atau items kosong
    });
}

// BENAR: Fail Fast di constructor dengan assertion (debug mode)
// dan validasi eksplisit untuk production
class Order {
    final String id;
    final String userId;
    final List<OrderItem> items;
    final double total;

    Order({
        required this.id,
        required this.userId,
        required this.items,
        required this.total,
    })  : assert(id.isNotEmpty, 'Order id must not be empty'),
          assert(userId.isNotEmpty, 'Order userId must not be empty'),
          assert(items.isNotEmpty, 'Order must have at least one item'),
          assert(total >= 0, 'Order total cannot be negative');

    // Named constructor dengan full validation untuk production
    factory Order.create({
        required String id,
        required String userId,
        required List<OrderItem> items,
    }) {
        if (id.isEmpty) throw ArgumentError('id must not be empty');
        if (userId.isEmpty) throw ArgumentError('userId must not be empty');
        if (items.isEmpty) throw ArgumentError('items must not be empty');

        final total = items.fold<double>(
            0, (sum, item) => sum + item.unitPrice * item.quantity,
        );

        return Order(id: id, userId: userId, items: items, total: total);
    }
}

// Fail Fast di service dengan early return
class OrderService {
    Future<String> createOrder({
        required String userId,
        required List<CartItem> cartItems,
    }) async {
        // Fail Fast: validasi semua sebelum operasi apapun
        if (userId.isEmpty) {
            throw ArgumentError('userId must not be empty');
        }
        if (cartItems.isEmpty) {
            throw StateError('cannot create order with empty cart');
        }

        // Cek user aktif sebelum mulai
        final user = await _userRepo.findById(userId);
        if (user == null) throw NotFoundException('user not found: $userId');
        if (!user.isActive) throw ForbiddenException('user account is not active');

        // Setelah semua validasi lulus, baru proses
        final order = Order.create(
            id: _generateId(),
            userId: userId,
            items: _mapToOrderItems(cartItems),
        );

        return _orderRepo.save(order);
    }
}

Fail Fast dan Testability #

Salah satu manfaat langsung Fail Fast yang sering tidak disadari adalah testability yang lebih baik. Kode yang Fail Fast lebih mudah ditest karena:

// Dengan Fail Fast, test bisa memverifikasi bahwa kondisi invalid
// ditolak dengan jelas dan cepat — bukan menghasilkan efek samping misterius

func TestTransferFunds_FailFast(t *testing.T) {
    tests := []struct {
        name    string
        fromID  string
        toID    string
        amount  float64
        wantErr string
    }{
        {
            name:    "empty fromID",
            fromID:  "",
            toID:    "acc-456",
            amount:  100,
            wantErr: "fromID is required",
        },
        {
            name:    "same account",
            fromID:  "acc-123",
            toID:    "acc-123",
            amount:  100,
            wantErr: "cannot transfer to the same account",
        },
        {
            name:    "negative amount",
            fromID:  "acc-123",
            toID:    "acc-456",
            amount:  -50,
            wantErr: "amount must be positive",
        },
        {
            name:    "zero amount",
            fromID:  "acc-123",
            toID:    "acc-456",
            amount:  0,
            wantErr: "amount must be positive",
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Test ini tidak butuh mock database sama sekali
            // karena Fail Fast terjadi sebelum database disentuh
            err := TransferFunds(context.Background(), tt.fromID, tt.toID, tt.amount)

            if err == nil {
                t.Fatal("expected error but got nil")
            }
            if !strings.Contains(err.Error(), tt.wantErr) {
                t.Errorf("error = %q, want to contain %q", err.Error(), tt.wantErr)
            }
        })
    }
}
// Test ini murni unit test — tidak butuh database, tidak butuh mock yang kompleks
// Karena validasi terjadi sebelum ada interaksi dengan dependency apapun

Kapan Fail Fast Berbahaya #

Fail Fast yang diterapkan tanpa mempertimbangkan konteks bisa menghasilkan sistem yang terlalu fragile — crash untuk hal-hal yang sebenarnya bisa di-handle dengan graceful degradation.

FAIL FAST YANG TEPAT:
  ✓ Startup — lebih baik tidak start daripada start dengan state salah
  ✓ Dependency injection — nil dependency adalah programmer error
  ✓ Domain invariant — data yang tidak valid tidak boleh masuk ke domain
  ✓ Exhaustive switch — unhandled case adalah programmer error
  ✓ Package-level initialization — regex, template, schema harus valid

FAIL FAST YANG BERBAHAYA (pertimbangkan Fail Safe atau graceful handling):
  ✗ Request user yang buruk — tolak dengan error yang jelas, jangan crash server
  ✗ Satu item gagal dari batch — fail satu item, lanjutkan yang lain
  ✗ Feature non-kritikal yang gagal — tampilkan fallback, jangan down semua
  ✗ Dependency eksternal yang tidak tersedia — circuit breaker + fallback
  ✗ Data lama yang tidak memenuhi validasi baru saat migrasi
    — update secara bertahap, jangan reject semua

PERTANYAAN YANG TEPAT sebelum memutuskan Fail Fast atau tidak:
  "Jika kondisi ini terjadi, apakah seluruh sistem tidak bisa berfungsi
   dengan benar jika kita lanjutkan?"
  → Ya: Fail Fast
  → Tidak, ada fallback yang masih memberikan nilai: Fail Safe
  → Tidak, caller bisa handle: return error
Fail Fast di library yang di-consume orang lain perlu pertimbangan ekstra. panic dari library tidak bisa dicegah oleh caller kecuali dengan recover yang eksplisit. Jika kamu menulis library publik, pertimbangkan untuk selalu return error daripada panic — kecuali untuk kondisi yang benar-benar tidak mungkin dipulihkan (misalnya regexp.MustCompile dengan regex yang hardcoded dan tidak pernah bisa invalid).

Anti-Pattern dalam Satu Pandangan #

// ✗ Validasi terlambat — sudah query DB sebelum cek input
func CreateUser(email, password string) error {
    hashedPw := hashPassword(password) // operasi berjalan dulu
    if email == "" { return errors.New("email required") } // validasi terlambat
    return db.Save(email, hashedPw)
}

// ✗ Swallow error tanpa fallback yang bermakna
func GetUserName(id string) string {
    user, err := db.FindUser(id)
    if err != nil {
        return "" // error diabaikan, caller tidak tahu ada masalah
    }
    return user.Name
}

// ✗ Aplikasi start meski dependency tidak tersedia
func main() {
    cfg := loadConfig() // tidak divalidasi
    db := connectDB(cfg.DSN) // mungkin nil jika DSN kosong
    startServer(db) // crash nanti saat pertama kali ada request
}

// ✗ panic untuk kondisi yang bisa terjadi secara normal
func FindOrder(id string) *Order {
    order, err := db.Find(id)
    if err != nil {
        panic(err) // kondisi yang valid (not found) dijadikan panic
    }
    return order
}

// ✗ Health check yang selalu sehat
func health(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("ok")) // tidak cek dependency — SPoF tersembunyi
}

// ✗ error tanpa konteks yang cukup
func processPayment(id string) error {
    _, err := db.FindPayment(id)
    if err != nil {
        return err // tidak ada konteks: operasi mana? ID apa?
    }
    return nil
}
// ✓ Seharusnya:
// return fmt.Errorf("processPayment %s: find payment: %w", id, err)

Checklist Review Fail Fast #

INPUT VALIDATION:
  □ Semua validasi input ada di awal fungsi — sebelum operasi apapun
  □ Error message jelas dan menyebut field mana yang bermasalah dan mengapa
  □ Tidak ada operasi yang berjalan dengan input yang tidak valid

STARTUP DAN INISIALISASI:
  □ Semua environment variable yang diperlukan divalidasi saat startup
  □ Koneksi ke dependency kritikal dicek sebelum server mulai serve traffic
  □ Package-level initialization menggunakan Must pattern untuk nilai yang
    seharusnya selalu valid

PANIC VS ERROR:
  □ panic hanya untuk programmer errors — invariant yang dilanggar, nil dependency
  □ error untuk kondisi runtime yang caller bisa dan harus handle
  □ Library tidak menggunakan panic untuk kondisi yang bisa terjadi secara normal

ARSITEKTUR:
  □ API error response mengandung code, message, dan trace ID yang cukup
    untuk debugging tanpa expose internal detail
  □ Domain model tidak bisa diinstansiasi dalam state yang tidak valid
  □ Health check endpoint mencerminkan kondisi nyata dari dependency

KOMBINASI DENGAN FAIL SAFE:
  □ Dependency eksternal punya circuit breaker atau fallback — bukan crash
  □ Feature non-kritikal punya degraded mode — bukan down semua
  □ Request user yang buruk mendapat error yang jelas — bukan server crash

Ringkasan #

  • Fail Fast berarti mendeteksi dan melaporkan kondisi tidak valid sesegera mungkin — sebelum state yang tidak valid menyebar lebih jauh. Jarak antara sumber error dan tempat error terdeteksi adalah biaya debugging yang tidak perlu.
  • Level 1 — Input validation: semua guard clause di awal fungsi, sebelum operasi apapun. Error yang spesifik menyebut field mana bermasalah dan mengapa.
  • Level 2 — Startup: validasi semua config dan koneksi ke dependency sebelum server mulai serve. Lebih baik tidak bisa start dengan pesan jelas daripada crash misterius di runtime.
  • Level 3 — panic vs error di Go: error untuk kondisi runtime yang caller bisa handle; panic untuk programmer errors dan invariant yang dilanggar. MustX pattern untuk inisialisasi yang seharusnya selalu berhasil.
  • Level 4 — Arsitektur: API error yang informatif dengan trace ID; domain model yang tidak bisa diinstansiasi dalam state invalid; health check yang mencerminkan kondisi dependency nyata.
  • Fail Fast vs Fail Safe: bukan pilihan yang eksklusif. Fail Fast untuk startup dan programmer errors; Fail Safe untuk dependency eksternal dan fitur non-kritikal yang punya fallback bermakna.
  • Fail Fast meningkatkan testability: validasi yang terjadi sebelum dependency disentuh bisa ditest sebagai pure unit test tanpa mock.
  • Kapan berbahaya: jangan Fail Fast untuk request user yang buruk (return error), untuk satu item gagal dari batch (skip dan lanjut), atau untuk dependency eksternal (circuit breaker + fallback).
  • Di library publik: hindari panic untuk kondisi yang bisa terjadi secara normal — caller tidak bisa mencegah panic dari library kecuali dengan recover yang eksplisit.

← Sebelumnya: SPoF   Berikutnya: SPOF →

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