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 int status (seperti printf, fclose, main): 0 atau nilai positif = sukses, -1 atau EOF (-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 global errno (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.

c
#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:

text
100 / 5 = 20
84 / 0 gagal: pembagi nol
-7 / 2 gagal: input negatif tidak diizinkan

Fungsi 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:

  1. Baris 5-8 (#define OK 0 ...): konstanta kode error didefinisikan dengan #define agar mudah dibaca. OK = 0, kode error mulai dari 1. Pemberian nama eksplisit mencegah “magic number” seperti return 2 yang tidak jelas maksudnya.
  2. Baris 10 (int bagi(int a, int b, int *hasil)): signature fungsi. Return type int dipakai untuk status, bukan untuk hasil perhitungan. Parameter ketiga int *hasil adalah output parameter—pointer ke variabel pemanggil yang akan diisi bila sukses.
  3. Baris 11-12 (if (b == 0) return ERR_DIV_ZERO;): early return pattern. Begitu kondisi error terdeteksi, fungsi langsung kembali tanpa melanjutkan. Bila b == 0, output parameter *hasil tidak ditulis—pemanggil tidak boleh membacanya.
  4. 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.
  5. Baris 15 (*hasil = a / b; return OK;): baru di jalur sukses, output parameter ditulis dan fungsi mengembalikan OK (0).
  6. Baris 28 (int rc = bagi(...);): pemanggil menangkap return code di variabel rc sebelum memeriksa. Pola if (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.

c
#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:

text
[OK]    /etc/passwd berhasil dibaca
[GAGAL] /etc/tidak_ada_xyz: No such file or directory

Fungsi 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.

c
#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:

text
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.