Dalam aplikasi nyata, eksekusi kode sering menelusuri rantai pemanggilan bertingkat: main memanggil proses_data, yang memanggil parse_angka, yang memanggil strtol. Bila strtol gagal, error harus naik dari level paling dalam sampai ke level yang paling tepat untuk menanganinya. Bahasa tanpa exception seperti C harus melakukannya secara eksplisit: setiap level memeriksa return code dari level di bawahnya, lalu memutuskan apakah menangani sendiri atau meneruskan ke atas. Pada chapter ini kita akan mempelajari tiga pola propagasi: propagasi sederhana lewat return code, propagasi dengan konteks tambahan, dan kebebasan pemanggil puncak untuk memilih strategi penanganan.

Pola 1: Propagasi Sederhana #

Pola paling dasar: setiap fungsi mengembalikan kode error, dan pemanggil langsung mengembalikan kode yang sama ke atas bila tidak bisa menangani. Tidak ada manipulasi pesan—hanya forwarding. Keuntungannya: kode ringkas, kodenya identik di setiap level. Kerugian: pesan error tidak bertambah konteks.

c
#include <stdio.h>
#include <stdlib.h>

/* Chain 3 fungsi: main -> proses_data -> baca_file / parse_angka.
   Setiap level mengembalikan kode error; pemanggil memutuskan
   apakah meneruskan ke atas atau menangani sendiri. */

typedef enum {
    E_OK = 0,
    E_FILE_TIDAK_ADA = 1,
    E_FILE_KOSONG = 2,
    E_PARSE_GAGAL = 3,
    E_OUT_OF_MEM = 4,
} err_t;

/* Level 3 (paling bawah): baca file */
err_t baca_file(const char *path, char *buf, size_t n) {
    FILE *fp = fopen(path, "r");
    if (fp == NULL) return E_FILE_TIDAK_ADA;

    if (fgets(buf, (int)n, fp) == NULL) {
        fclose(fp);
        return E_FILE_KOSONG;
    }
    fclose(fp);
    return E_OK;
}

/* Level 2: parse angka dari string */
err_t parse_angka(const char *s, int *out) {
    char *end;
    long v = strtol(s, &end, 10);
    if (end == s || (*end != '\n' && *end != '\0')) return E_PARSE_GAGAL;
    if (v < 0 || v > 1000000) return E_PARSE_GAGAL;
    *out = (int)v;
    return E_OK;
}

/* Level 1: gabungkan baca+parse */
err_t proses_data(const char *path, int *out) {
    char buf[64];
    err_t e = baca_file(path, buf, sizeof(buf));
    if (e != E_OK) {
        /* Propagasikan error ke pemanggil tanpa konsumsi resource */
        return e;
    }
    return parse_angka(buf, out);
}

int main(void) {
    const char *paths[] = {
        "/tmp/propagasi_tidak_ada.txt",
        "/tmp/propagasi_kosong.txt",
        "/tmp/propagasi_bukan_angka.txt",
        "/tmp/propagasi_ok.txt"
    };

    /* Setup file demo */
    FILE *f1 = fopen(paths[1], "w"); if (f1) fclose(f1);
    FILE *f2 = fopen(paths[2], "w"); if (f2) { fprintf(f2, "bukan_angka\n"); fclose(f2); }
    FILE *f3 = fopen(paths[3], "w"); if (f3) { fprintf(f3, "42\n"); fclose(f3); }

    int n = sizeof(paths) / sizeof(paths[0]);
    for (int i = 0; i < n; i++) {
        int val;
        err_t e = proses_data(paths[i], &val);
        if (e == E_OK) {
            printf("proses_data('%s') -> OK, nilai=%d\n", paths[i], val);
        } else {
            const char *alasan = "tidak dikenal";
            switch (e) {
                case E_FILE_TIDAK_ADA: alasan = "file tidak ada"; break;
                case E_FILE_KOSONG:    alasan = "file kosong"; break;
                case E_PARSE_GAGAL:    alasan = "parse gagal"; break;
                case E_OUT_OF_MEM:    alasan = "out of memory"; break;
                default: break;
            }
            printf("proses_data('%s') -> GAGAL: %s\n", paths[i], alasan);
        }
    }
    return 0;
}

Output yang Dihasilkan:

text
proses_data('/tmp/propagasi_tidak_ada.txt') -> GAGAL: file tidak ada
proses_data('/tmp/propagasi_kosong.txt') -> GAGAL: file kosong
proses_data('/tmp/propagasi_bukan_angka.txt') -> GAGAL: parse gagal
proses_data('/tmp/propagasi_ok.txt') -> OK, nilai=42

Empat skenario uji menunjukkan error dari level berbeda berhasil naik ke main. Saat fopen gagal (E_FILE_TIDAK_ADA), baca_file mengembalikan kode itu ke proses_data, yang langsung meneruskannya ke main tanpa modifikasi. Saat strtol gagal (E_PARSE_GAGAL), parse_angka mengembalikan kode itu, kembali naik lewat proses_data ke main.

Mari kita telaah baris-baris kode di atas:

  1. Baris 8-14 (typedef enum { E_OK, E_FILE_TIDAK_ADA, ... } err_t;): enum tunggal yang dipakai di SEMUA level. Inilah kunci propagasi sederhana—kode yang sama bermakna sama di setiap level. Bila setiap level punya enum sendiri, pemanggil harus melakukan mapping, dan propagasi tidak lagi sederhana.
  2. Baris 17-27 (err_t baca_file(...)): level paling bawah. Tidak ada pemanggil lain di bawahnya—fopen dan fgets adalah library call yang menetapkan errno, tetapi di sini kita konversi ke kode enum sendiri.
  3. Baris 30-37 (err_t parse_angka(...)): level 2. Memanggil strtol (library) dan memeriksa end pointer. Bila end == s, tidak ada karakter yang diparse—berarti input bukan angka. Pemanggil tidak tahu file mana yang bermasalah, hanya tahu ada masalah parsing.
  4. Baris 40-48 (err_t proses_data(...)): level 1. Menggabungkan baca_file dan parse_angka. Bila baca_file gagal (e != E_OK), langsung return e tanpa memanggil parse_angka. Inilah short-circuit propagation: bila langkah A gagal, jangan lanjut ke langkah B.
  5. Baris 71-87 (switch (e) ...): pemanggil puncak (main) menerjemahkan kode enum menjadi teks untuk pengguna. Di sinilah kode error akhirnya “ditangani”—dipresentasikan ke pengguna. Strategi penanganan bisa berbeda-beda di setiap pemanggil, seperti yang akan kita lihat di pola 3.

Pola 2: Propagasi dengan Konteks #

Pola kedua memperkaya informasi error saat naik. Setiap level menambahkan konteks tentang apa yang sedang dilakukannya saat error terjadi. Teknik ini sering dipakai di logging framework: error dari strtol (“parse gagal”) diperkaya menjadi “parse_angka gagal di baris 42 file config.txt”.

c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Error propagation dengan konteks: setiap level menambah info
   tentang lokasi/penyebab error. Output parameter error_msg
   menyimpan teks yang bisa dicetak pemanggil puncak. */

typedef enum {
    E_OK = 0,
    E_ALLOC = 1,
    E_OVERFLOW = 2,
    E_EMPTY = 3,
} level2_status;

typedef struct {
    level2_status kode;
    char pesan[128];   /* teks error spesifik per-level */
} error_info;

/* Level bawah: tambah elemen ke array dinamis */
level2_status array_tambah(int **arr, size_t *n, size_t *kap, int nilai) {
    if (*n == *kap) {
        size_t baru = (*kap == 0) ? 4 : *kap * 2;
        int *tmp = realloc(*arr, sizeof(int) * baru);
        if (tmp == NULL) return E_ALLOC;
        *arr = tmp;
        *kap = baru;
    }
    (*arr)[(*n)++] = nilai;
    return E_OK;
}

/* Level tengah: jumlahkan array */
level2_status sum_array(const int *arr, size_t n, long *out) {
    if (arr == NULL || n == 0) return E_EMPTY;
    long total = 0;
    for (size_t i = 0; i < n; i++) {
        total += arr[i];
        if (total > 1000000) return E_OVERFLOW;
    }
    *out = total;
    return E_OK;
}

/* Level atas: gabungkan, kembalikan error_info dengan konteks */
error_info proses(int **arr, size_t *n, size_t *kap) {
    error_info info = {0};
    int nilai_input[] = {10, 20, 30, 40, 50};

    for (size_t i = 0; i < 5; i++) {
        level2_status s = array_tambah(arr, n, kap, nilai_input[i]);
        if (s != E_OK) {
            info.kode = s;
            snprintf(info.pesan, sizeof(info.pesan),
                     "array_tambah gagal di iterasi %zu (kode %d)", i, s);
            return info;
        }
    }

    long total;
    level2_status s = sum_array(*arr, *n, &total);
    if (s != E_OK) {
        info.kode = s;
        snprintf(info.pesan, sizeof(info.pesan),
                 "sum_array gagal (kode %d, n=%zu)", s, *n);
        return info;
    }

    info.kode = E_OK;
    snprintf(info.pesan, sizeof(info.pesan), "sukses, total=%ld", total);
    return info;
}

int main(void) {
    int *arr = NULL;
    size_t n = 0, kap = 0;

    error_info info = proses(&arr, &n, &kap);
    printf("proses() -> kode=%d, pesan='%s'\n", info.kode, info.pesan);
    if (info.kode == E_OK) {
        printf("Array (%zu elemen):", n);
        for (size_t i = 0; i < n; i++) printf(" %d", arr[i]);
        printf("\n");
    }

    free(arr);
    return 0;
}

Output yang Dihasilkan:

text
proses() -> kode=0, pesan='sukses, total=150'
Array (5 elemen): 10 20 30 40 50

error_info menggabungkan kode error (level2_status) dengan teks pesan (pesan[128]). Saat array_tambah gagal di iterasi ke-2, pesan akan berbunyi "array_tambah gagal di iterasi 2 (kode 1)"—jauh lebih informatif daripada hanya kode E_ALLOC. Fungsi snprintf di sini membatasi penulisan ke 128 byte, mencegah buffer overflow yang merupakan risiko klasik saat memformat teks error.

Pola 3: Pemanggil Memutuskan Strategi #

Pola ketiga menunjukkan keuntungan utama propagasi: pemanggil puncak bebas memutuskan apa yang dilakukan dengan error yang diterima. Fungsi load_config yang sama bisa dipanggil dengan tiga strategi berbeda: (A) gunakan nilai default, (B) coba path alternatif, atau (C) hentikan program.

c
#include <stdio.h>
#include <stdlib.h>

/* Demonstrasi: caller dapat MEMUTUSKAN apa yang dilakukan dengan error.
   Function yang sama dapat dipanggil dengan strategi error berbeda. */

typedef enum {
    LOAD_OK = 0,
    LOAD_NOT_FOUND,
    LOAD_INVALID,
} load_status;

load_status load_config(const char *path, int *out) {
    FILE *fp = fopen(path, "r");
    if (fp == NULL) return LOAD_NOT_FOUND;

    char buf[64];
    if (fgets(buf, sizeof(buf), fp) == NULL) {
        fclose(fp);
        return LOAD_INVALID;
    }
    fclose(fp);

    int v = atoi(buf);
    if (v <= 0) return LOAD_INVALID;
    *out = v;
    return LOAD_OK;
}

/* Strategi A: cetak error, kembalikan default */
int strategi_default(const char *path, int fallback) {
    int val;
    load_status s = load_config(path, &val);
    if (s == LOAD_OK) return val;

    switch (s) {
        case LOAD_NOT_FOUND: fprintf(stderr, "warning: %s tidak ada, pakai default %d\n", path, fallback); break;
        case LOAD_INVALID:   fprintf(stderr, "warning: %s tidak valid, pakai default %d\n", path, fallback); break;
        default: break;
    }
    return fallback;
}

/* Strategi B: retry dengan path alternatif */
int strategi_retry(const char *path1, const char *path2) {
    int val;
    load_status s = load_config(path1, &val);
    if (s == LOAD_OK) {
        printf("[retry] loaded dari %s\n", path1);
        return val;
    }
    printf("[retry] %s gagal (kode %d), coba %s\n", path1, s, path2);

    s = load_config(path2, &val);
    if (s == LOAD_OK) {
        printf("[retry] loaded dari %s\n", path2);
        return val;
    }
    printf("[retry] %s juga gagal (kode %d)\n", path2, s);
    return -1;
}

/* Strategi C: abort program */
int strategi_abort(const char *path) {
    int val;
    load_status s = load_config(path, &val);
    if (s == LOAD_OK) return val;
    fprintf(stderr, "[abort] FATAL: %s tidak bisa dimuat (kode %d)\n", path, s);
    exit(1);
}

int main(void) {
    /* Setup */
    FILE *ok = fopen("/tmp/propagasi_ok.txt", "w");
    if (ok) { fprintf(ok, "42\n"); fclose(ok); }

    /* Strategi A: gunakan default */
    printf("=== Strategi A (default) ===\n");
    int v1 = strategi_default("/tmp/propagasi_tidak_ada.txt", 100);
    printf("hasil = %d\n\n", v1);

    /* Strategi B: retry */
    printf("=== Strategi B (retry) ===\n");
    int v2 = strategi_retry("/tmp/propagasi_tidak_ada.txt", "/tmp/propagasi_ok.txt");
    printf("hasil = %d\n\n", v2);

    /* Strategi C: abort - tidak dipanggil agar demo tetap jalan */
    printf("=== Strategi C (abort) ===\n");
    printf("(demonstrasi: kode yang sama bisa dipakai dengan strategi berbeda)\n");
    (void)strategi_abort;   /* suppress unused warning */
    return 0;
}

Output yang Dihasilkan:

text
=== Strategi A (default) ===
hasil = 100

=== Strategi B (retry) ===
[retry] /tmp/propagasi_tidak_ada.txt gagal (kode 1), coba /tmp/propagasi_ok.txt
[retry] loaded dari /tmp/propagasi_ok.txt
hasil = 42

=== Strategi C (abort) ===
(demonstrasi: kode yang sama bisa dipakai dengan strategi berbeda)

Stderr (dari strategi A):

text
warning: /tmp/propagasi_tidak_ada.txt tidak ada, pakai default 100

Tiga strategi memakai load_config yang persis sama, tetapi menghasilkan perilaku berbeda. Strategi A meneruskan eksekusi dengan nilai default 100 bila file tidak ada. Strategi B mencoba path alternatif dan mendapatkan 42. Strategi C (bila dipanggil) akan menghentikan program dengan exit(1). Inilah inti error propagation di C: fungsi pelapor tidak mengambil keputusan—hanya memberi informasi. Keputusan diambil di level yang paling memahami konteks penggunaan.