Hingga bagian ini, operasi CRUD dan persistensi sudah berfungsi. Namun program belum siap dipakai bila pengguna bisa memasukkan harga negatif, nama kosong, atau id diluar rentang tanpa feedback yang jelas. Pada bagian ini, kita perdalam strategi validasi dan error handling agar program tetap stabil saat input tidak valid, dan pengguna tahu persis apa yang salah.

Konsep #

Strategi validasi yang kita pegang adalah tiga lapis:

  1. Lapis model — product_is_valid() memeriksa field satu per satu. Fungsi ini dipanggil setelah product_create dan sebelum menyimpan ke file.
  2. Lapis koleksi — inventory_add/update/delete memvalidasi argumen sebelum menyentuh array. Bila validasi gagal, fungsi mengembalikan kode error dan tidak mengubah state.
  3. Lapis UI — main.c menangkap kode error dari koleksi dan mencetak pesan yang dapat dimengerti pengguna.

Mekanisme kerja kode error: kita gunakan konvensi “0 = sukses, negatif = gagal”. Setiap kode negatif bisa berarti jenis error berbeda. Misalnya product_create mengembalikan:

  • 0 bila sukses
  • -1 bila pointer NULL atau nama kosong
  • -2 bila harga invalid
  • -3 bila stok negatif
  • -4 bila id invalid

Pemanggil bisa memeriksa kode spesifik untuk memberikan pesan error yang akurat.

Implementasi #

Definisi kode error di product.c:

c
/* Kode error yang dipakai product_create */
#define PRODUCT_ERR_NAME   (-1)
#define PRODUCT_ERR_PRICE  (-2)
#define PRODUCT_ERR_STOCK  (-3)
#define PRODUCT_ERR_ID     (-4)

Validasi di product_create dengan feedback kode error:

c
int product_create(Product *p, int id, const char *name,
                   double price, int stock) {
    if (p == NULL || name == NULL) {
        return PRODUCT_ERR_NAME;
    }
    if (id <= 0 || id > PRODUCT_ID_MAX) {
        return PRODUCT_ERR_ID;
    }
    if (price < 0.0 || isnan(price) || isinf(price)) {
        return PRODUCT_ERR_PRICE;
    }
    if (stock < 0) {
        return PRODUCT_ERR_STOCK;
    }
    /* ... isi field ... */
    return 0;
}

Di main.c, kita tangkap kode error dan cetak pesan yang berguna:

c
static void do_add(Inventory *inv) {
    char name[PRODUCT_NAME_MAX];
    char price_buf[32];
    char stock_buf[16];

    printf("Nama produk: ");
    if (read_line(name, sizeof(name)) != 0) {
        return;
    }
    printf("Harga (Rp): ");
    read_line(price_buf, sizeof(price_buf));
    printf("Stok: ");
    read_line(stock_buf, sizeof(stock_buf));

    double price = strtod(price_buf, NULL);
    int stock = (int)strtol(stock_buf, NULL, 10);
    int id = inventory_add(inv, name, price, stock);
    if (id < 0) {
        printf("Gagal menambah produk. Periksa kembali input Anda.\n");
        return;
    }
    printf("Produk ditambahkan dengan id=%d\n", id);
}

Pengamanan tambahan: parsing input pengguna dengan strtod/strtol tidak crash pada input non-angka (cuma mengembalikan 0). Tetapi kita harus memeriksa apakah konversi benar-benar terjadi dengan endptr:

c
static int parse_double(const char *s, double *out) {
    if (s == NULL || out == NULL || s[0] == '\0') {
        return -1;
    }
    char *endptr = NULL;
    double v = strtod(s, &endptr);
    if (endptr == s || *endptr != '\0') {
        return -1; /* ada karakter yang bukan digit */
    }
    *out = v;
    return 0;
}

Verifikasi error handling dengan kasus berbagai input invalid:

c
/* demo_error.c - Uji coba error handling */
#include <stdio.h>
#include "inventory.h"

int main(void) {
    Inventory inv;
    inventory_init(&inv);

    /* Kasus 1: harga negatif */
    int id = inventory_add(&inv, "Produk A", -100.0, 5);
    printf("harga negatif -> id=%d (harus -1)\n", id);

    /* Kasus 2: nama kosong */
    id = inventory_add(&inv, "", 1000.0, 5);
    printf("nama kosong -> id=%d (harus -1)\n", id);

    /* Kasus 3: stok negatif */
    id = inventory_add(&inv, "Produk B", 1000.0, -3);
    printf("stok negatif -> id=%d (harus -1)\n", id);

    /* Kasus 4: data valid */
    id = inventory_add(&inv, "Produk Valid", 1000.0, 3);
    printf("valid -> id=%d (harus 1)\n", id);

    /* Kasus 5: update id yang tidak ada */
    int rc = inventory_update(&inv, 999, 2000.0, 5);
    printf("update id tidak ada -> rc=%d (harus -1)\n", rc);

    /* Kasus 6: delete id yang tidak ada */
    rc = inventory_delete(&inv, 999);
    printf("delete id tidak ada -> rc=%d (harus -1)\n", rc);
    return 0;
}

Output yang Dihasilkan:

text
harga negatif -> id=-1 (harus -1)
nama kosong -> id=-1 (harus -1)
stok negatif -> id=-1 (harus -1)
valid -> id=1 (harus 1)
update id tidak ada -> rc=-1 (harus -1)
delete id tidak ada -> rc=-1 (harus -1)

Mari kita telaah baris-baris kode di atas:

  1. Baris 1-5 (konstanta kode error): Macro didefinisikan dengan nilai negatif yang berbeda untuk setiap jenis kesalahan. Pemanggil bisa membandingkan dengan kode spesifik untuk memberi pesan yang akurat.
  2. Baris 8-13 (NULL check pertama): Pengecekan pointer NULL dilakukan paling awal. Bila tidak ada, name[0] bisa segfault.
  3. Baris 14-16 (id check): Validasi rentang id. Nilai id <= 0 menolak id 0 dan negatif. Nilai id > PRODUCT_ID_MAX mencegah id berlebihan yang bisa overflow di %06d format.
  4. Baris 17-19 (price check): isnan dan isinf dari <math.h> mendeteksi hasil 0.0/0.0 dan 1.0/0.0 yang valid di IEEE-754 tetapi tidak boleh jadi harga.
  5. Baris 29-30 (parse_double endptr check): strtod mengembalikan endptr yang menunjuk ke karakter pertama yang tidak diproses. Bila endptr == s, tidak ada karakter yang diproses (input bukan angka). Bila *endptr != '\0', ada karakter sisa (mis. "12.5abc").
  6. Baris 47-49 (UI feedback): printf dengan kode error spesifik membantu pengguna mengoreksi input. Hindari pesan generik “input salah” yang tidak informatif.

Kode Lengkap #

Kode lengkap dengan error handling terdapat pada product.c, inventory.c, dan main.c. Fungsi parse_double dan parse_int dengan endptr check bisa ditempatkan di file helper terpisah bila dipakai banyak modul.

Pengujian #

Pengujian error handling memerlukan edge case berlimpah. Daftar test yang harus dijalankan:

  • Input "" (string kosong) untuk nama → ditolak.
  • Input "abc" untuk harga → ditolak.
  • Input "100.5.5" untuk harga → ditolak.
  • Input sangat besar 99999999999999999999 → overflow, ditolak.
  • Input dengan whitespace awal " 100" → diterima (strtol handle leading whitespace).
  • File CSV dengan baris rusak → dilewati tanpa crash.
bash
gcc -std=c11 -Wall -Wextra -o /tmp/demo_error demo_error.c \
    inventory.c product.c -lm
/tmp/demo_error