Ketika fopen mengembalikan NULL, pertanyaan pertama kita adalah “kenapa?”. File tidak ada? Tidak ada izin? Path terlalu panjang? Disk penuh? Jawabannya ada di variabel global errno yang diset oleh system call yang gagal, dan pesan teks yang mudah dibaca bisa didapat dengan perror atau strerror. Pada chapter ini kita akan membuka file di path yang sengaja tidak ada, melihat bagaimana errno diisi, dan mengapa kita wajib menyimpannya segera sebelum memanggil fungsi lain. Pemahaman ini krusial untuk menulis program yang memberikan pesan error berguna kepada pengguna.

Kontrak errno, perror, strerror #

ISO/IEC 9899 §7.5 mendefinisikan errno sebagai makro yang berkembang menjadi lvalue bertipe int modifiable. Kontrak penting:

  • errno tidak pernah di-reset ke 0 oleh library C — bila sebuah fungsi berhasil, errno tidak diubah. Karena itu tidak pernah periksa errno untuk menentukan keberhasilan—selalu periksa return value fungsi.
  • errno diset ke nilai non-zero saat fungsi gagal — nilai spesifik bergantung pada kesalahan (lihat <errno.h> dan POSIX <errno.h>).
  • perror(const char *s) mencetak s: <pesan error>\n ke stderr, di mana <pesan error> diterjemahkan dari errno saat ini.
  • strerror(int errnum) mengembalikan string yang berisi pesan untuk kode error yang diberikan, tanpa mencetaknya—berguna bila kita ingin memformat pesan sendiri.

POSIX.1 menambahkan banyak kode errno standar: ENOENT (2, No such file or directory), EACCES (13, Permission denied), EEXIST (17, File exists), EBADF (9, Bad file descriptor), ENOSPC (28, No space left on device), dan seterusnya.

Pola Aman: Cek Return Value, Simpan errno #

Pola yang benar adalah:

  1. Periksa return value fungsi untuk mengetahui keberhasilan.
  2. Bila gagal, segera salin errno ke variabel lokal (misalnya int saved_errno = errno;).
  3. Cetak pesan error dengan perror atau strerror(saved_errno).

Langkah ke-2 sangat penting karena banyak fungsi library (termasuk printf, perror, strerror sendiri) dapat mengubah errno secara internal. Tanpa penyimpanan, pesan yang kita dapat bisa tidak sesuai dengan kesalahan asli.

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

int main(void) {
    /* Demo 1: fopen gagal membuka file tidak ada */
    errno = 0;
    FILE *f = fopen("/nonexistent/path/file.txt", "r");
    if (f == NULL) {
        int saved = errno;
        printf("fopen gagal, errno=%d\n", saved);
        perror("fopen /nonexistent/path/file.txt");
        printf("strerror: %s\n", strerror(saved));
    }
    if (f) fclose(f);

    /* Demo 2: fwrite ke stream read-only */
    f = fopen("/tmp/test_errno.txt", "w");
    if (f) {
        fputs("hello\n", f);
        fclose(f);
    }
    f = fopen("/tmp/test_errno.txt", "r");
    if (f) {
        int rc = fputs("ini akan gagal\n", f);
        if (rc == EOF) {
            int saved = errno;
            printf("\nfputs ke read-only stream gagal, errno=%d\n", saved);
            perror("fputs");
        }
        fclose(f);
    }

    /* Demo 3: open(2) langsung dari POSIX */
    errno = 0;
    int fd = open("/tmp/nonexistent_dir_xyz/file.txt", O_WRONLY | O_CREAT, 0644);
    if (fd < 0) {
        int saved = errno;
        printf("\nopen(2) gagal, errno=%d\n", saved);
        perror("open");
    } else {
        close(fd);
    }

    return 0;
}

Output yang Dihasilkan:

text
fopen gagal, errno=2
fopen /nonexistent/path/file.txt: No such file or directory
strerror: No such file or directory

fputs ke read-only stream gagal, errno=9
fputs: Bad file descriptor

open(2) gagal, errno=2
open: No such file or directory

Tiga demo di atas mengilustrasikan skenario error yang berbeda:

  • Demo 1: fopen membuka path yang tidak ada di mode "r". Kernel mengembalikan ENOENT (errno=2, No such file or directory). Pesan dari perror dan strerror konsisten.
  • Demo 2: kita menulis ke file yang dibuka dengan mode "r" (read-only). fputs gagal, errno diisi EBADF (9, Bad file descriptor). Pesan perror di sini adalah “Bad file descriptor” karena C standard library menggunakan file descriptor yang ditandai read-only.
  • Demo 3: kita memanggil system call open(2) langsung untuk membuat file di direktori yang tidak ada. Kernel mengembalikan ENOENT (errno=2).

Mari kita telaah baris-baris kode di atas:

  1. Baris 9 (errno = 0;): kita reset errno sebelum panggilan fungsi yang ingin diuji. Bila fopen berhasil, errno tetap 0. Bila gagal, errno akan diset kernel. (Catatan: standar C tidak menjamin errno di-reset pada keberhasilan—praktik reset manual sebelum pemanggilan adalah konvensi yang aman.)
  2. Baris 12 (int saved = errno;): penyimpanan errno ke variabel lokal. Ini krusial karena printf di baris 13 dapat mengubah errno secara internal (misalnya bila buffer penuh). Tanpa penyimpanan, perror di baris 14 mungkin melaporkan error yang berbeda.
  3. Baris 14 (perror("fopen /nonexistent/path/file.txt");): perror mencetak argumen + ": " + pesan error dari errno saat ini + \n ke stderr. perror sendiri dapat mengubah errno—karena itu kita pakai saved pada strerror di baris 15.
  4. Baris 26 (int rc = fputs("ini akan gagal\n", f);): fputs mengembalikan non-negatif bila berhasil, EOF bila gagal. Karena stream dibuka dengan mode "r", kernel menolak operasi tulis.
  5. Baris 35 (int fd = open(..., O_WRONLY | O_CREAT, 0644);): open(2) adalah system call POSIX langsung (bukan C standard library). Mengembalikan file descriptor non-negatif bila berhasil, -1 bila gagal dengan errno diset.
  6. Baris 39 (perror("open");): kita menyimpan errno di saved lebih dulu (baris 38) untuk berjaga-jaga. Dalam demo ini perror benar melaporkan No such file or directory.

Bukti: perror Mengubah errno #

Untuk membuktikan bahwa perror (dan banyak fungsi lain) dapat mengubah errno, mari kita jalankan eksperimen sederhana: buka file yang tidak ada, lalu cetak errno sebelum dan sesudah perror.

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

int main(void) {
    errno = 0;
    FILE *f = fopen("/nonexistent/path/file.txt", "r");
    if (f == NULL) {
        int saved = errno;
        printf("errno setelah fopen: %d\n", errno);
        perror("perror pertama");
        printf("errno setelah perror: %d\n", errno);
        printf("strerror(saved_errno): %s\n", strerror(saved));
        printf("strerror(errno): %s\n", strerror(errno));
    }
    if (f) fclose(f);
    return 0;
}

Output yang Dihasilkan:

text
perror pertama: No such file or directory
errno setelah fopen: 2
errno setelah perror: 22
strerror(saved_errno): No such file or directory
strerror(errno): Invalid argument

Lihat output di atas. Setelah fopen gagal, errno=2 (No such file or directory). Tetapi setelah perror dipanggil, errno=22 (Invalid argument)—perror internal memanggil fungsi yang mengubah errno! Akibatnya, strerror(errno) di baris terakhir melaporkan “Invalid argument” yang bukan error asli dari fopen. Hanya strerror(saved) (menggunakan variabel yang kita simpan lebih awal) yang melaporkan pesan yang benar.

Inilah mengapa aturan “selalu simpan errno segera setelah panggilan yang gagal” wajib diikuti. Bila kita memanggil fungsi lain di antara (termasuk printf, perror, atau bahkan alokasi memori), errno bisa berubah dan pesan error yang kita laporkan menjadi menyesatkan.

Kode errno Umum di Linux #

Beberapa kode errno yang sering ditemui dalam operasi file di Linux:

errnoKonstantaPesanPenyebab umum
2ENOENTNo such file or directoryFile/direktori tidak ada
13EACCESPermission deniedTidak punya izin
17EEXISTFile existsFile sudah ada (mode wx gagal)
9EBADFBad file descriptorfclose dua kali, atau operasi tidak sesuai mode
28ENOSPCNo space left on deviceDisk penuh
24EMFILEToo many open filesBatas file descriptor tercapai
36ENAMETOOLONGFile name too longPath melebihi PATH_MAX
40ETIMEDOUTConnection timed outTimeout pada network file system
22EINVALInvalid argumentArgumen fungsi tidak valid