Banyak fungsi di standard library C mengembalikan nilai sederhana seperti -1 atau NULL saat gagal, tetapi nilai itu sendiri tidak menjelaskan apa yang salah. fopen mengembalikan NULL baik karena file tidak ada, tidak ada izin baca, atau path terlalu panjang. Untuk memberi detail lebih, C menyediakan variabel global errno (dideklarasikan di <errno.h>) yang berisi kode integer kesalahan terakhir yang ditinggalkan oleh library call. Pada chapter ini kita akan belajar bagaimana cara membaca errno, kapan harus meresetnya, dan jebakan klasik yang sering menjebak pemula. Topik ini diatur di ISO/IEC 9899 §7.5.

Apa itu errno #

errno adalah thread-local variabel (sejak C11) bertipe int. Saat sebuah fungsi library gagal, ia menetapkan kode error positif ke errno. Saat fungsi sukses, perilaku standar adalah tidak mengubah errno—ini penting dan akan kita bahas sebagai jebakan utama. Header <errno.h> mendefinisikan banyak konstanta kode error, antara lain:

  • EDOM — domain error dalam fungsi matematika (misal sqrt(-1)).
  • ERANGE — range error dalam fungsi matematika/konversi (misal strtol overflow).
  • EILSEQ — illegal byte sequence (misal wide character tidak valid).
  • EINVAL — invalid argument.
  • ENOENT — no such file or directory (kode 2 di glibc).

Nilai numerik errno bersifat implementation-defined—ENOENT bisa 2 di Linux, tetapi nilai berbeda di Windows. Inilah sebabnya kita selalu memakai nama konstanta (ENOENT), bukan angka mentah.

Membaca errno setelah fopen Gagal #

Skenario paling umum: kita mencoba membuka file yang tidak ada. fopen mengembalikan NULL dan menetapkan errno = ENOENT. Mari kita baca kode dan artinya.

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

int main(void) {
    /* Simpan errno lama untuk pemulihan (opsional, hanya demonstrasi) */
    int err_lama = errno;
    printf("errno awal (belum ada operasi): %d\n", err_lama);

    /* Coba buka file yang tidak ada */
    FILE *fp = fopen("/etc/tidak_ada_123", "r");
    if (fp == NULL) {
        printf("fopen gagal\n");
        printf("errno = %d\n", errno);
        printf("arti  = %s\n", strerror(errno));
    } else {
        fclose(fp);
    }
    return 0;
}

Output yang Dihasilkan:

text
errno awal (belum ada operasi): 0
fopen gagal
errno = 2
arti  = No such file or directory

Pada eksekusi pertama sebelum operasi file, errno bernilai 0 yang berarti “tidak ada error”. Setelah fopen("/etc/tidak_ada_123", "r") mengembalikan NULL, errno berubah menjadi 2 (ENOENT di glibc). Fungsi strerror() dari <string.h> mengkonversi angka itu menjadi teks yang bisa dibaca manusia: "No such file or directory".

Mari kita telaah baris-baris kode di atas:

  1. Baris 6 (int err_lama = errno;): menyimpan nilai errno saat ini sebelum operasi baru. Berguna bila kita ingin memulihkan errno lama setelah memanggil fungsi yang mungkin mengubahnya (misal printf bisa mengubah errno bila I/O gagal).
  2. Baris 7 (printf("errno awal...")): mencetak nilai awal. Di Linux glibc, program baru dimulai dengan errno == 0, tetapi standar tidak menjamin ini—kita tetap harus mereset bila ingin memastikan.
  3. Baris 10 (FILE *fp = fopen(...)): memanggil fopen. Bila gagal, return value NULL dan errno ditetapkan oleh fopen. Urutan ini penting: cek dulu return value-nya, baru baca errno.
  4. Baris 11 (if (fp == NULL)): keputusan “gagal atau sukses” selalu diambil dari return value, bukan dari errno. Standar C tidak menjanjikan errno == 0 saat sukses.
  5. Baris 12-14 (printf(... errno ... strerror(errno))): mencetak kode numerik errno dan terjemahan teksnya. strerror mengembalikan pointer ke string statis—tidak perlu free.

Reset errno Sebelum Memanggil Fungsi #

Saat kita ingin tahu apakah sebuah fungsi spesifik mengalami error tertentu, kita harus mereset errno = 0; sebelum memanggilnya. Tanpa reset, errno bisa berisi sisa dari operasi sebelumnya. Pola ini sangat penting untuk fungsi seperti strtol yang return value-nya bisa valid meski ada error (misal 0 adalah hasil yang sah, tetapi juga hasil bila input tidak valid).

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

int main(void) {
    /* Demonstrasi ERANGE: strtol terhadap angka terlalu besar */
    errno = 0;   /* WAJIB: reset sebelum memanggil fungsi yang menggunakan errno */

    char *besar = "999999999999999999999999999999";
    long val = strtol(besar, NULL, 10);

    if (errno == ERANGE) {
        printf("str strtol melebihi batas\n");
        printf("errno = %d (ERANGE)\n", errno);
        printf("arti  = %s\n", strerror(errno));
        printf("val   = %ld (LONG_MAX dari limits.h)\n", val);
    } else {
        printf("val = %ld\n", val);
    }

    /* Reset errno, lalu demonstrasi EDOM lewat sqrt(-1) */
    errno = 0;
    double r = sqrt(-1.0);
    if (errno == EDOM) {
        printf("\nsqrt(-1) menghasilkan domain error\n");
        printf("errno = %d (EDOM)\n", errno);
        printf("arti  = %s\n", strerror(errno));
        printf("hasil = %f\n", r);
    }
    return 0;
}

Output yang Dihasilkan:

text
str strtol melebihi batas
errno = 34 (ERANGE)
arti  = Numerical result out of range
val   = 9223372036854775807 (LONG_MAX dari limits.h)

sqrt(-1) menghasilkan domain error
errno = 33 (EDOM)
arti  = Numerical argument out of domain
hasil = -nan

strtol("999999999999999999999999999999", ...) tidak muat di long, sehingga errno ditetapkan ke ERANGE (34 di Linux glibc) dan return value dijepit ke LONG_MAX. Tanpa errno = 0 di awal, kita tidak akan tahu apakah errno == ERANGE berasal dari strtol ini atau dari operasi sebelumnya. Demonstrasi kedua menggunakan sqrt(-1.0) yang menetapkan EDOM (33) dan mengembalikan NaN.

Jebakan Klasik: errno Tidak Direset oleh Sukses #

Mari kita buktikan secara empiris bahwa fungsi yang sukses tidak menjamin errno menjadi 0. Kita memanggil fopen yang gagal (mengubah errno), lalu memanggil fopen yang sukses dan melihat errno tetap menyimpan kode lama.

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

/* Jebakan klasik: errno hanya valid tepat setelah fungsi gagal.
   Operasi sukses TIDAK dijamin mereset errno ke 0. */
int main(void) {
    /* Step 1: buat errno berisi sesuatu */
    FILE *fp1 = fopen("/etc/tidak_ada_a", "r");
    if (fp1 == NULL) {
        printf("Step 1 fopen gagal, errno = %d (%s)\n", errno, strerror(errno));
    }

    /* Step 2: lakukan operasi yang PASTI sukses */
    FILE *fp2 = fopen("/etc/hostname", "r");
    if (fp2 != NULL) {
        printf("Step 2 fopen '/etc/hostname' sukses\n");
        printf("         errno SETELAH sukses = %d (%s)\n",
               errno, strerror(errno));
        fclose(fp2);
    }

    /* Pelajaran: untuk memeriksa sukses, andalkan RETURN VALUE,
       bukan nilai errno. errno hanya bermakna jika return value sudah error. */
    return 0;
}

Output yang Dihasilkan:

text
Step 1 fopen gagal, errno = 2 (No such file or directory)
Step 2 fopen '/etc/hostname' sukses
         errno SETELAH sukses = 2 (No such file or directory)

Perhatikan: fopen("/etc/hostname") berhasil membuka file (terlihat dari fp2 != NULL), tetapi errno tetap 2 dari kegagalan sebelumnya. Inilah bukti bahwa kita tidak boleh memakai errno sebagai indikator sukses. Aturan praktis: errno adalah side channel—ia bermakna hanya jika return value sudah memberi tahu kita bahwa ada error.