Bahasa C tidak memiliki exception seperti Java atau Python. Tidak ada try/catch, tidak ada throw, dan tidak ada stack unwinding otomatis. Saat sebuah fungsi menghadapi kondisi yang tidak bisa ditangani, satu-satunya mekanisme standar untuk memberi tahu pemanggil adalah lewat return value. Pada chapter ini kita akan mempelajari konvensi return code yang dipakai di seluruh ekosistem C: 0 berarti sukses, nilai non-zero berarti kode error spesifik. Konvensi ini sederhana, tetapi memengaruhi cara kita merancang setiap function signature.
Konvensi Dasar: 0 = Sukses, Non-Zero = Error #
Konvensi return code di C berasal dari sejarah UNIX: program yang berakhir dengan exit status 0 dianggap sukses, sedangkan 1-255 menandakan kegagalan. Konvensi yang sama diterapkan ke function return value di seluruh standard library:
- Fungsi yang mengembalikan
intstatus (sepertiprintf,fclose,main):0atau nilai positif = sukses,-1atauEOF(-1) = gagal. - Fungsi yang mengembalikan pointer (seperti
malloc,fopen):NULL= gagal, pointer non-NULL= sukses. Tidak ada kode error di return value—detail kesalahan disimpan di variabel globalerrno(lihat chapter terpisah).
Pola yang akan kita pakai di seluruh kode kita sendiri adalah: return value bertipe int atau enum, di mana 0 = sukses dan setiap nilai non-zero memetakan satu jenis kesalahan berbeda.
Fungsi dengan Banyak Kode Error #
Kita akan mulai dengan contoh konkret. Fungsi bagi() berikut menerima dua bilangan dan menulis hasil pembagian ke output parameter. Return value mengikuti konvensi 0 sukses, non-zero kode error spesifik.
#include <stdio.h>
#include <stddef.h>
/* Konvensi: 0 = sukses, non-zero = kode error spesifik */
#define OK 0
#define ERR_DIV_ZERO 1
#define ERR_NEG_INPUT 2
int bagi(int a, int b, int *hasil) {
if (b == 0) return ERR_DIV_ZERO;
if (a < 0 || b < 0) return ERR_NEG_INPUT;
*hasil = a / b;
return OK;
}
int main(void) {
int kasus[][2] = {
{100, 5}, /* OK */
{84, 0}, /* bagi nol */
{-7, 2} /* input negatif */
};
int n = sizeof(kasus) / sizeof(kasus[0]);
for (int i = 0; i < n; i++) {
int hasil = 0;
int rc = bagi(kasus[i][0], kasus[i][1], &hasil);
if (rc == OK) {
printf("%d / %d = %d\n", kasus[i][0], kasus[i][1], hasil);
} else if (rc == ERR_DIV_ZERO) {
printf("%d / %d gagal: pembagi nol\n", kasus[i][0], kasus[i][1]);
} else if (rc == ERR_NEG_INPUT) {
printf("%d / %d gagal: input negatif tidak diizinkan\n",
kasus[i][0], kasus[i][1]);
}
}
return 0;
}Output yang Dihasilkan:
100 / 5 = 20
84 / 0 gagal: pembagi nol
-7 / 2 gagal: input negatif tidak diizinkanFungsi bagi() tidak bisa mengembalikan hasil pembagian secara langsung karena harus tetap memberi tahu pemanggil bila terjadi error. Solusinya adalah memisahkan: return value untuk status, output parameter (int *hasil) untuk hasil perhitungan saat sukses. Pemanggil wajib memeriksa rc sebelum membaca hasil.
Mari kita telaah baris-baris kode di atas:
- Baris 5-8 (
#define OK 0 ...): konstanta kode error didefinisikan dengan#defineagar mudah dibaca.OK= 0, kode error mulai dari 1. Pemberian nama eksplisit mencegah “magic number” sepertireturn 2yang tidak jelas maksudnya. - Baris 10 (
int bagi(int a, int b, int *hasil)): signature fungsi. Return typeintdipakai untuk status, bukan untuk hasil perhitungan. Parameter ketigaint *hasiladalah output parameter—pointer ke variabel pemanggil yang akan diisi bila sukses. - Baris 11-12 (
if (b == 0) return ERR_DIV_ZERO;): early return pattern. Begitu kondisi error terdeteksi, fungsi langsung kembali tanpa melanjutkan. Bilab == 0, output parameter*hasiltidak ditulis—pemanggil tidak boleh membacanya. - Baris 13 (
if (a < 0 || b < 0) return ERR_NEG_INPUT;): contoh aturan domain bisnis. Bisa saja tidak ada—tergantung kebutuhan. Yang penting setiap kondisi mengembalikan kode yang berbeda. - Baris 15 (
*hasil = a / b; return OK;): baru di jalur sukses, output parameter ditulis dan fungsi mengembalikanOK(0). - Baris 28 (
int rc = bagi(...);): pemanggil menangkap return code di variabelrcsebelum memeriksa. Polaif (rc == OK) { ... } else if (rc == ERR_...)memastikan setiap cabang error ditangani.
Konvensi Sistem: -1 = Gagal, Lihat errno #
Standard library C menggunakan variasi konvensi: fungsi seperti fopen, fclose, read mengembalikan -1 (atau NULL untuk fungsi pointer) saat gagal, dan menetapkan kode error ke variabel global errno. Pemanggil memeriksa -1 lalu membaca errno untuk mengetahui penyebab.
#include <stdio.h>
#include <errno.h>
#include <string.h>
/* Konvensi sistem: 0 = sukses, -1 = gagal (lihat errno) */
static int baca_konfigurasi(const char *path) {
FILE *fp = fopen(path, "r");
if (fp == NULL) return -1;
/* (di dunia nyata: parse isi file di sini) */
fclose(fp);
return 0;
}
int main(void) {
const char *file = "/etc/passwd"; /* biasanya ada */
const char *file2 = "/etc/tidak_ada_xyz"; /* pasti tidak ada */
if (baca_konfigurasi(file) == 0) {
printf("[OK] %s berhasil dibaca\n", file);
} else {
printf("[GAGAL] %s: %s\n", file, strerror(errno));
}
if (baca_konfigurasi(file2) == 0) {
printf("[OK] %s berhasil dibaca\n", file2);
} else {
printf("[GAGAL] %s: %s\n", file2, strerror(errno));
}
return 0;
}Output yang Dihasilkan:
[OK] /etc/passwd berhasil dibaca
[GAGAL] /etc/tidak_ada_xyz: No such file or directoryFungsi baca_konfigurasi() membungkus fopen. Bila fopen mengembalikan NULL, fungsi mengembalikan -1 ke pemanggil. Pemanggil tidak tahu persis apa yang salah—errno menyimpan kode error yang ditinggalkan oleh fopen. Fungsi strerror(errno) (dari <string.h>) mengkonversi kode numerik menjadi teks yang dapat dibaca manusia, dalam contoh ini "No such file or directory".
Menggunakan enum Agar Kode Error Lebih Ekspresif #
Pada proyek yang lebih besar, daftar #define menjadi sulit dikelola. Kita dapat memakai enum yang memberikan namespace semantik lewat nama constant.
#include <stdio.h>
#include <stdlib.h>
/* Enum untuk return code - lebih ekspresif dibanding #define */
typedef enum {
CACHE_OK = 0,
CACHE_MISS = 1,
CACHE_INVALID = 2,
CACHE_ALLOC_FAIL = 3,
} cache_status;
static int *cache = NULL;
static int cache_size = 0;
cache_status cache_init(int size) {
if (size <= 0) return CACHE_INVALID;
cache = malloc(sizeof(int) * (size_t)size);
if (cache == NULL) return CACHE_ALLOC_FAIL;
cache_size = size;
for (int i = 0; i < size; i++) cache[i] = -1;
return CACHE_OK;
}
cache_status cache_get(int index, int *out) {
if (cache == NULL) return CACHE_INVALID;
if (index < 0 || index >= cache_size) return CACHE_INVALID;
if (cache[index] == -1) return CACHE_MISS;
*out = cache[index];
return CACHE_OK;
}
int main(void) {
cache_status s = cache_init(4);
if (s != CACHE_OK) {
fprintf(stderr, "cache_init gagal: %d\n", s);
return 1;
}
cache[1] = 42; /* tulis satu elemen */
cache[2] = 99;
int val;
for (int i = 0; i < 4; i++) {
s = cache_get(i, &val);
switch (s) {
case CACHE_OK: printf("cache[%d] = %d\n", i, val); break;
case CACHE_MISS: printf("cache[%d] = (kosong)\n", i); break;
default: printf("cache[%d] = ERROR %d\n", i, s); break;
}
}
free(cache);
cache = NULL;
return 0;
}Output yang Dihasilkan:
cache[0] = (kosong)
cache[1] = 42
cache[2] = 99
cache[3] = (kosong)Fungsi cache_get() bisa mengembalikan empat status berbeda: CACHE_OK, CACHE_MISS, CACHE_INVALID, atau implicit lewat default case. Pemanggil menggunakan switch untuk memisahkan setiap cabang. Return type cache_status membuat signature fungsi mendokumentasikan diri sendiri—pembaca tahu persis daftar status yang mungkin.