Setelah mempelajari return code, errno, perror/strerror, assert, defensive programming, cleanup, dan propagation di chapter-chapter sebelumnya, kita sampai pada pertanyaan praktis: bagaimana menyatukan semua teknik ini menjadi satu strategi yang konsisten untuk seluruh proyek? Pada chapter ini, kita akan membandingkan tiga pola desain utama (return code, errno, output parameter), melihat contoh implementasi yang konsisten, dan menyusun aturan praktis kapan memakai assert versus return code. Tujuan akhirnya: pembaca kode kita tidak perlu menebak apakah sebuah fungsi melaporkan error lewat return value, output parameter, atau errno.
Tiga Pola Desain Utama #
Tiga pola desain yang akan kita bandingkan:
- Return code sederhana: fungsi mengembalikan
int(misal0sukses,-1gagal). Sederhana, tetapi detail kesalahan hilang. - errno: fungsi mengembalikan
0/-1, detail kesalahan dierrno. Mengikuti konvensi standard library, tetapi thread-unsafe bila dipakai tidak hati-hati. - Output parameter untuk struktur error: fungsi mengembalikan status code, dan pemanggil yang ingin detail mengoper pointer ke struct error. Memberi konteks paling kaya.
Mari kita lihat ketiganya diimplementasikan untuk fungsi yang sama: parse string ke integer dengan batasan [0, 1000].
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/* Tiga pola desain error handling yang berbeda untuk fungsi yang sama.
Fungsi mengkonversi string ke integer dengan batasan. */
/* POLA A: return code sederhana.
Return value berfungsi ganda: kode error ATAU nilai hasil.
Masalah: 0 sering ambigu (sukses atau nilai valid?). */
int parse_pola_a(const char *s, int *out) {
if (s == NULL || *s == '\0') return -1;
char *end;
long v = strtol(s, &end, 10);
if (*end != '\0' || v < 0 || v > 1000) return -2;
*out = (int)v;
return 0;
}
/* POLA B: errno untuk detail.
Return value tetap int (0/-1), errno menjelaskan penyebab. */
int parse_pola_b(const char *s, int *out) {
errno = 0;
if (s == NULL || *s == '\0') { errno = EINVAL; return -1; }
char *end;
long v = strtol(s, &end, 10);
if (errno == ERANGE) { return -1; }
if (*end != '\0') { errno = EINVAL; return -1; }
if (v < 0 || v > 1000) { errno = ERANGE; return -1; }
*out = (int)v;
return 0;
}
/* POLA C: output parameter untuk struktur error kaya. */
typedef enum {
PARSE_OK = 0,
PARSE_NULL_INPUT,
PARSE_EMPTY,
PARSE_NON_DIGIT,
PARSE_OVERFLOW,
PARSE_OUT_OF_RANGE,
} parse_result;
typedef struct {
parse_result kode;
const char *pesan;
size_t posisi; /* posisi karakter yang bermasalah */
} parse_error;
parse_result parse_pola_c(const char *s, int *out, parse_error *err) {
if (s == NULL) {
if (err) { err->kode = PARSE_NULL_INPUT; err->pesan = "input NULL"; err->posisi = 0; }
return PARSE_NULL_INPUT;
}
if (*s == '\0') {
if (err) { err->kode = PARSE_EMPTY; err->pesan = "input kosong"; err->posisi = 0; }
return PARSE_EMPTY;
}
char *end;
long v = strtol(s, &end, 10);
if (*end != '\0') {
if (err) {
err->kode = PARSE_NON_DIGIT;
err->pesan = "karakter non-digit ditemukan";
err->posisi = (size_t)(end - s);
}
return PARSE_NON_DIGIT;
}
if (v < 0 || v > 1000) {
if (err) {
err->kode = PARSE_OUT_OF_RANGE;
err->pesan = "nilai di luar rentang [0, 1000]";
err->posisi = 0;
}
return PARSE_OUT_OF_RANGE;
}
*out = (int)v;
return PARSE_OK;
}
int main(void) {
const char *kasus[] = {"42", "abc", "5000", "", NULL};
int n = sizeof(kasus) / sizeof(kasus[0]);
printf("=== Pola A: return code sederhana ===\n");
for (int i = 0; i < n; i++) {
int val;
int rc = parse_pola_a(kasus[i], &val);
const char *s = kasus[i] ? kasus[i] : "(NULL)";
if (rc == 0) printf(" parse('%s') = %d\n", s, val);
else printf(" parse('%s') GAGAL rc=%d\n", s, rc);
}
printf("\n=== Pola B: errno untuk detail ===\n");
for (int i = 0; i < n; i++) {
int val;
int rc = parse_pola_b(kasus[i], &val);
const char *s = kasus[i] ? kasus[i] : "(NULL)";
if (rc == 0) printf(" parse('%s') = %d\n", s, val);
else printf(" parse('%s') GAGAL errno=%d (%s)\n",
s, errno, strerror(errno));
}
printf("\n=== Pola C: output parameter error_info ===\n");
for (int i = 0; i < n; i++) {
int val;
parse_error err;
parse_result r = parse_pola_c(kasus[i], &val, &err);
const char *s = kasus[i] ? kasus[i] : "(NULL)";
if (r == PARSE_OK) {
printf(" parse('%s') = %d\n", s, val);
} else {
printf(" parse('%s') GAGAL: %s (kode %d, posisi %zu)\n",
s, err.pesan, err.kode, err.posisi);
}
}
return 0;
}Output yang Dihasilkan:
=== Pola A: return code sederhana ===
parse('42') = 42
parse('abc') GAGAL rc=-2
parse('5000') GAGAL rc=-2
parse('') GAGAL rc=-1
parse('(NULL)') GAGAL rc=-1
=== Pola B: errno untuk detail ===
parse('42') = 42
parse('abc') GAGAL errno=22 (Invalid argument)
parse('5000') GAGAL errno=34 (Numerical result out of range)
parse('') GAGAL errno=22 (Invalid argument)
parse('(NULL)') GAGAL errno=22 (Invalid argument)
=== Pola C: output parameter error_info ===
parse('42') = 42
parse('abc') GAGAL: karakter non-digit ditemukan (kode 3, posisi 0)
parse('5000') GAGAL: nilai di luar rentang [0, 1000] (kode 5, posisi 0)
parse('') GAGAL: input kosong (kode 2, posisi 0)
parse('(NULL)') GAGAL: input NULL (kode 1, posisi 0)Perbandingan output membuktikan perbedaan informasi yang diberikan tiap pola. Pola A hanya membedakan “input kosong/NULL” (-1) dari “input tidak valid lainnya” (-2), tidak bisa membedakan "abc" dari "5000". Pola B menggunakan errno standar (EINVAL atau ERANGE) sehingga setidaknya membedakan “invalid argument” dari “out of range”. Pola C paling informatif: kode error spesifik, pesan deskriptif, dan posisi karakter bermasalah.
Mari kita telaah baris-baris kode di atas:
- Baris 9-19 (
parse_pola_a): pola paling sederhana. Return valueintdipakai untuk status (0atau-1/-2), hasil disimpan di output parameter. Pola ini cukup bila pemanggil hanya perlu tahu “berhasil atau tidak” tanpa detail. - Baris 21-30 (
parse_pola_b): pola mengikuti konvensi standard library.errno = 0di awal, laluEINVALatauERANGEditetapkan sesuai jenis kesalahan. Pemanggil membacaerrnountuk detail—pola yang sama sepertistrtolasli. - Baris 41-77 (
parse_pola_c): pola paling kaya. Fungsi mengembalikanparse_result(enum) dan mengisi output parameterparse_error *errbila pemanggil mau. Fieldposisimemberi informasi yang tidak mungkin diberikan pola A/B: di mana persisnya karakter bermasalah berada. - Baris 63 (
err->posisi = (size_t)(end - s);): aritmatika pointer.end - smenghasilkan jumlah karakter dari awal string sampai titik parsing berhenti. Pola C bisa memberi info ini; pola A dan B tidak.
Desain Konsisten di Satu Modul #
Saat kita membangun sebuah modul (misal string_t dengan fungsi init, append, free), konsistensi adalah kunci. Setiap fungsi dalam modul harus mengikuti pola yang sama: tipe return yang sama, output parameter error pada posisi yang sama, makro pembantu yang sama. Inilah ciri modul yang dirancang dengan baik—bukan sekadar kumpulan fungsi yang masing-masing bekerja sendiri.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Contoh desain error handling yang konsisten di satu modul.
Konvensi: setiap fungsi mengembalikan my_status_t.
Bila gagal, detail isi di my_error_t (output parameter opsional). */
typedef enum {
MY_OK = 0,
MY_ERR_NULL = 1,
MY_ERR_INVALID = 2,
MY_ERR_NOT_FOUND = 3,
MY_ERR_ALLOC = 4,
MY_ERR_RANGE = 5,
} my_status_t;
typedef struct {
my_status_t kode;
const char *fungsi; /* nama fungsi yang melaporkan */
const char *konteks; /* info tambahan bebas */
} my_error_t;
/* Macro untuk membuat error dengan info konteks.
Catatan: parameter macro namanya _kode_val agar tidak bentrok
dengan nama field 'kode' di my_error_t. */
#define MAKE_ERROR(err_ptr, kode_val, fnc, ctx) \
do { \
if (err_ptr) { \
(err_ptr)->kode = (kode_val); \
(err_ptr)->fungsi = (fnc); \
(err_ptr)->konteks = (ctx); \
} \
} while (0)
typedef struct {
char *buf;
size_t panjang;
} string_t;
/* Fungsi inisialisasi dengan pola yang konsisten */
my_status_t string_init(string_t *s, const char *init, my_error_t *err) {
if (s == NULL) { MAKE_ERROR(err, MY_ERR_NULL, "string_init", "s == NULL"); return MY_ERR_NULL; }
if (init == NULL) { MAKE_ERROR(err, MY_ERR_NULL, "string_init", "init == NULL"); return MY_ERR_NULL; }
size_t len = strlen(init);
s->buf = malloc(len + 1);
if (s->buf == NULL) {
MAKE_ERROR(err, MY_ERR_ALLOC, "string_init", "malloc gagal");
return MY_ERR_ALLOC;
}
memcpy(s->buf, init, len + 1);
s->panjang = len;
return MY_OK;
}
my_status_t string_append(string_t *s, const char *tambahan, my_error_t *err) {
if (s == NULL || tambahan == NULL) {
MAKE_ERROR(err, MY_ERR_NULL, "string_append", "argumen NULL");
return MY_ERR_NULL;
}
size_t add_len = strlen(tambahan);
size_t baru = s->panjang + add_len;
char *tmp = realloc(s->buf, baru + 1);
if (tmp == NULL) {
MAKE_ERROR(err, MY_ERR_ALLOC, "string_append", "realloc gagal");
return MY_ERR_ALLOC;
}
s->buf = tmp;
memcpy(s->buf + s->panjang, tambahan, add_len + 1);
s->panjang = baru;
return MY_OK;
}
void string_free(string_t *s) {
if (s == NULL) return;
free(s->buf);
s->buf = NULL;
s->panjang = 0;
}
void print_error(const my_error_t *err) {
if (err == NULL) return;
const char *alasan = "tidak dikenal";
switch (err->kode) {
case MY_ERR_NULL: alasan = "argumen NULL"; break;
case MY_ERR_INVALID: alasan = "input tidak valid"; break;
case MY_ERR_NOT_FOUND: alasan = "tidak ditemukan"; break;
case MY_ERR_ALLOC: alasan = "gagal alokasi memori"; break;
case MY_ERR_RANGE: alasan = "di luar rentang"; break;
default: break;
}
fprintf(stderr, " ERROR di %s: %s (%s)\n", err->fungsi, alasan, err->konteks);
}
int main(void) {
string_t s;
my_error_t err;
/* Sukses */
my_status_t r = string_init(&s, "Halo, ", &err);
printf("init('Halo, ') -> %d\n", r);
if (r != MY_OK) print_error(&err);
r = string_append(&s, "dunia!", &err);
printf("append('dunia!') -> %d, isi sekarang: '%s'\n", r, s.buf);
/* Error: argumen NULL */
r = string_append(&s, NULL, &err);
printf("append(NULL) -> %d\n", r);
if (r != MY_OK) print_error(&err);
/* Error: pointer string_t NULL */
r = string_init(NULL, "test", &err);
printf("init(NULL) -> %d\n", r);
if (r != MY_OK) print_error(&err);
string_free(&s);
return 0;
}Output yang Dihasilkan:
init('Halo, ') -> 0
append('dunia!') -> 0, isi sekarang: 'Halo, dunia!'
append(NULL) -> 1
init(NULL) -> 1Stderr:
ERROR di string_append: argumen NULL (argumen NULL)
ERROR di string_init: argumen NULL (s == NULL)Modul string_t mengikuti satu konvensi: setiap fungsi publik (string_init, string_append) mengembalikan my_status_t dan menerima output parameter opsional my_error_t *err. Fungsi string_free yang tidak mungkin gagal tidak mengembalikan status—konsisten dengan konvensi bahwa fungsi void tidak butuh error reporting. Macro MAKE_ERROR menstandarkan cara mengisi my_error_t, sehingga setiap titik error di modul terlihat seragam.
Aturan Praktis: assert vs Return Code #
Pertanyaan desain yang sering muncul: kapan memakai assert, kapan memakai return code? Aturan praktis: assert untuk bug programmer, return code untuk kondisi yang sah di runtime. Mari kita lihat implementasi stack yang membedakan keduanya dengan jelas.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Demonstrasi: kapan pakai assert vs kapan pakai return code.
Aturan umum: assert untuk bug programmer, return code untuk
kondisi yang sah di runtime. */
typedef enum {
STACK_OK = 0,
STACK_FULL,
STACK_EMPTY,
STACK_ALLOC_FAIL,
} stack_status;
typedef struct {
int *data;
size_t kapasitas;
size_t ukuran;
} stack_t;
stack_status stack_init(stack_t *s, size_t kapasitas) {
/* Parameter kapasitas == 0 adalah bug programmer - pakai assert */
/* Parameter s == NULL juga bug programmer - pakai assert */
/* Tidak ada alasan valid untuk memanggil stack_init(NULL, 5) */
/* Alloc failure adalah kondisi runtime yang sah - pakai return code */
s->data = malloc(sizeof(int) * kapasitas);
if (s->data == NULL) return STACK_ALLOC_FAIL;
s->kapasitas = kapasitas;
s->ukuran = 0;
return STACK_OK;
}
stack_status stack_push(stack_t *s, int val) {
/* Stack penuh adalah kondisi runtime yang sah - return code */
if (s->ukuran == s->kapasitas) return STACK_FULL;
s->data[s->ukuran++] = val;
return STACK_OK;
}
stack_status stack_pop(stack_t *s, int *out) {
/* Stack kosong adalah kondisi runtime yang sah - return code */
if (s->ukuran == 0) return STACK_EMPTY;
*out = s->data[--s->ukuran];
return STACK_OK;
}
void stack_free(stack_t *s) {
if (s && s->data) {
free(s->data);
s->data = NULL;
s->ukuran = s->kapasitas = 0;
}
}
int main(void) {
stack_t s;
stack_status r = stack_init(&s, 3);
if (r != STACK_OK) {
fprintf(stderr, "init gagal: %d\n", r);
return 1;
}
int values[] = {10, 20, 30, 40}; /* nilai ke-4 akan gagal */
for (int i = 0; i < 4; i++) {
r = stack_push(&s, values[i]);
if (r == STACK_OK) {
printf("push(%d) -> OK (ukuran=%zu)\n", values[i], s.ukuran);
} else {
printf("push(%d) -> GAGAL (stack penuh, kode %d)\n", values[i], r);
}
}
/* Pop semua elemen */
printf("\nIsi stack (LIFO):\n");
int v;
while ((r = stack_pop(&s, &v)) == STACK_OK) {
printf(" pop -> %d (sisa=%zu)\n", v, s.ukuran);
}
printf("pop pada stack kosong -> kode %d\n", r);
stack_free(&s);
return 0;
}Output yang Dihasilkan:
push(10) -> OK (ukuran=1)
push(20) -> OK (ukuran=2)
push(30) -> OK (ukuran=3)
push(40) -> GAGAL (stack penuh, kode 1)
Isi stack (LIFO):
pop -> 30 (sisa=2)
pop -> 20 (sisa=1)
pop -> 10 (sisa=0)
pop pada stack kosong -> kode 2Implementasi stack_t menerapkan dua kategori error:
assert(sebaiknya ditambahkan): parameters == NULL, kapasitas== 0. Ini bug programmer—tidak ada penggunaan yang sah daristack_init(NULL, 5).assertakan memanggilabort()saat development build.return code:STACK_FULLsaat stack penuh,STACK_EMPTYsaat pop pada stack kosong,STACK_ALLOC_FAILsaatmallocgagal. Ketiganya adalah kondisi runtime yang sah—pemanggil harus bisa menanganinya.
Checklist Desain Error Handling #
Sebelum menutup section error handling, berikut checklist yang bisa kita pakai untuk meninjau desain error handling di setiap modul baru:
- Satu konvensi per modul: tidak mencampur pola A, B, C di modul yang sama.
- Enum untuk kode error: lebih mudah dibaca daripada
#define, mendukung warning-Wswitch. 0= sukses, non-zero = error: konvensi standar yang akrab bagi programmer C.- Validasi di boundary: input dari luar divalidasi; di dalam modul, asumsikan valid.
assertuntuk bug programmer: tidak untuk error yang sah di runtime.- Cleanup dengan
goto: bila ada banyak resource, polagoto cleanupmenghindari duplikasi. - Pemanggil memutuskan strategi: fungsi pelapor tidak mencetak, tidak
exit. - Simpan
errnobila dipakai berulang: polaint saved_errno = errno;sebelum pemanggilan lain. - Dokumentasikan konvensi di header file: pemanggil harus tahu apa yang diharapkan tanpa membaca implementasi.