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 (misalsqrt(-1)).ERANGE— range error dalam fungsi matematika/konversi (misalstrtoloverflow).EILSEQ— illegal byte sequence (misal wide character tidak valid).EINVAL— invalid argument.ENOENT— no such file or directory (kode2di 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.
#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:
errno awal (belum ada operasi): 0
fopen gagal
errno = 2
arti = No such file or directoryPada 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:
- Baris 6 (
int err_lama = errno;): menyimpan nilaierrnosaat ini sebelum operasi baru. Berguna bila kita ingin memulihkanerrnolama setelah memanggil fungsi yang mungkin mengubahnya (misalprintfbisa mengubaherrnobila I/O gagal). - Baris 7 (
printf("errno awal...")): mencetak nilai awal. Di Linux glibc, program baru dimulai denganerrno == 0, tetapi standar tidak menjamin ini—kita tetap harus mereset bila ingin memastikan. - Baris 10 (
FILE *fp = fopen(...)): memanggilfopen. Bila gagal, return valueNULLdanerrnoditetapkan olehfopen. Urutan ini penting: cek dulu return value-nya, baru bacaerrno. - Baris 11 (
if (fp == NULL)): keputusan “gagal atau sukses” selalu diambil dari return value, bukan darierrno. Standar C tidak menjanjikanerrno == 0saat sukses. - Baris 12-14 (
printf(... errno ... strerror(errno))): mencetak kode numerikerrnodan terjemahan teksnya.strerrormengembalikan pointer ke string statis—tidak perlufree.
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).
#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:
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 = -nanstrtol("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.
#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:
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.