Membaca errno = 2 di log sudah memberi tahu kita bahwa ada error, tetapi angka itu tidak menjelaskan apa yang terjadi. Untuk memahami insiden di produksi, kita butuh teks seperti “No such file or directory” atau “Permission denied”. Standard library C menyediakan dua fungsi untuk mengkonversi errno menjadi teks yang bisa dibaca manusia: perror() yang mencetak langsung ke stderr dengan format siap pakai, dan strerror() yang mengembalikan string untuk kita format sendiri. Pada chapter ini kita akan membandingkan keduanya dan belajar pola aman saat memakainya.

perror: Cetak Prefix + Pesan Error ke stderr #

Fungsi perror dideklarasikan di <stdio.h>:

c
void perror(const char *s);

Perilakunya sederhana: jika s bukan NULL dan bukan empty string, perror mencetak s, lalu ": ", lalu deskripsi error dari errno, lalu '\n'. Semua keluaran ditulis ke stderr—bukan stdout—agar tidak tercampur dengan output normal program.

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

int main(void) {
    FILE *fp = fopen("/etc/file_demo_tidak_ada", "r");
    if (fp == NULL) {
        /* perror mencetak prefix + ": " + deskripsi error dari errno */
        perror("fopen(/etc/file_demo_tidak_ada)");
        return 1;
    }
    fclose(fp);
    return 0;
}

Output yang Dihasilkan:

text
fopen(/etc/file_demo_tidak_ada): No such file or directory

Pesan fopen(/etc/file_demo_tidak_ada): No such file or directory terdiri dari tiga bagian: prefix "fopen(/etc/file_demo_tidak_ada)" yang kita berikan, pemisah ": " otomatis, dan terjemahan errno yaitu "No such file or directory". Inilah cara tercepat untuk mencetak pesan error yang informatif di C.

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (FILE *fp = fopen(...)): fopen gagal karena file tidak ada, mengembalikan NULL dan menetapkan errno = ENOENT (2 di glibc).
  2. Baris 6 (if (fp == NULL)): pengecekan return value—bukan errno—yang menentukan jalur error. Standar C mengizinkan fungsi sukses meninggalkan errno non-zero, jadi errno bukan indikator sukses/gagal.
  3. Baris 8 (perror("fopen(...)")): argumen s adalah prefix. perror membaca errno saat ini (yang masih ENOENT karena tidak ada operasi lain yang mengubahnya), menerjemahkannya ke teks, dan mencetak ke stderr.
  4. Baris 9 (return 1;): keluar dengan exit code non-zero agar shell script pemanggil bisa mendeteksi kegagalan.

strerror: Format Pesan Secara Bebas #

Fungsi strerror dideklarasikan di <string.h>:

c
char *strerror(int errnum);

Berbeda dengan perror, strerror mengembalikan pointer ke string deskripsi error. Kita bebas memformatnya bersama data lain, mencetaknya ke file log yang berbeda, atau menyertakannya dalam struktur data error.

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

/* strerror memberi fleksibilitas formatting yang tidak dimiliki perror */
int main(void) {
    const char *paths[] = {
        "/etc/tidak_ada_a",
        "/root/.bashrc",            /* biasanya ada, tapi mungkin tidak bisa dibaca */
        "/etc/hostname"             /* biasanya ada dan bisa dibaca */
    };
    int n = sizeof(paths) / sizeof(paths[0]);

    for (int i = 0; i < n; i++) {
        FILE *fp = fopen(paths[i], "r");
        if (fp == NULL) {
            /* Format custom: gabungkan dengan info lain */
            fprintf(stderr, "[gagal] %s -> errno=%d pesan='%s'\n",
                    paths[i], errno, strerror(errno));
        } else {
            fprintf(stderr, "[ok]    %s dibuka\n", paths[i]);
            fclose(fp);
        }
    }
    return 0;
}

Output yang Dihasilkan:

text
[gagal] /etc/tidak_ada_a -> errno=2 pesan='No such file or directory'
[gagal] /root/.bashrc -> errno=13 pesan='Permission denied'
[ok]    /etc/hostname dibuka

Dengan strerror, kita bisa mencetak kode numerik errno (errno=2) berdampingan dengan deskripsi teks ('No such file or directory') dalam satu baris. Pesan ini jauh lebih informatif untuk log analysis dibanding output perror yang formatnya kaku. Output juga menunjukkan dua error berbeda dari fopen: kode 2 (ENOENT) untuk file yang tidak ada, dan kode 13 (EACCES) untuk file yang ada tetapi tidak bisa dibaca tanpa izin.

Membandingkan perror dan strerror #

Tabel ringkas perbedaan:

Aspekperror(s)strerror(errno)
OutputLangsung ke stderrMengembalikan char* untuk di-format
Formats: <msg>\n (otomatis)Bebas, kita tentukan
Header<stdio.h><string.h>
Sumber errorMembaca errno saat iniMenerima argumen int
Cocok untukPesan error sederhanaLog terstruktur, custom format

Keduanya sama-sama memakai tabel pesan internal dari library, sehingga hasil terjemahannya konsisten. Pilihan antara perror atau strerror adalah soal styling output: perror untuk pesan singkat saat command-line program, strerror untuk logging yang butuh konteks tambahan.

Pola Aman: Simpan errno Sebelum Memanggil Apa Pun #

perror dan strerror adalah fungsi seperti yang lain—mereka boleh mengubah errno saat dipanggil (ISO/IEC 9899 §7.5p3). Akibatnya, bila kita memanggil perror lalu strerror(errno) berurutan, strerror bisa membaca errno yang sudah berubah dan menampilkan pesan yang salah. Pola aman: simpan errno ke variabel lokal segera setelah fungsi gagal, lalu pakai variabel itu di semua pemanggilan berikutnya.

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

/* Pola yang benar: simpan errno ke variabel lokal sebelum memanggil
   fungsi lain (termasuk perror) yang mungkin mengubah errno. */
int main(void) {
    /* Kasus 1: file tidak ada */
    FILE *fp = fopen("/etc/xyz_tidak_ada", "r");
    if (fp == NULL) {
        int saved_errno = errno;          /* simpan SEGERA */

        /* perror: otomatis menulis ke stderr, format "prefix: <msg>" */
        perror("Kasus 1 (perror)");

        /* strerror pakai nilai yang disimpan, bukan errno yang mungkin sudah berubah */
        fprintf(stderr, "Kasus 1 (strerror): kode=%d arti=%s\n",
                saved_errno, strerror(saved_errno));
    } else {
        fclose(fp);
    }

    /* Kasus 2: malloc gagal (simulasi alokasi sangat besar) */
    errno = 0;
    size_t ukuran_raksasa = (size_t)-1;   /* SIZE_MAX */
    void *p = malloc(ukuran_raksasa);
    if (p == NULL) {
        int saved_errno = errno;
        perror("\nKasus 2 malloc (perror)");
        fprintf(stderr, "Kasus 2 malloc (strerror): kode=%d arti=%s\n",
                saved_errno, strerror(saved_errno));
    } else {
        free(p);
    }
    return 0;
}

Output yang Dihasilkan:

text
Kasus 1 (perror): No such file or directory
Kasus 1 (strerror): kode=2 arti=No such file or directory

Kasus 2 malloc (perror): Cannot allocate memory
Kasus 2 malloc (strerror): kode=12 arti=Cannot allocate memory

Kedua pesan (dari perror dan dari strerror) menampilkan kode dan arti yang sama persis karena keduanya membaca saved_errno, bukan errno yang mungkin sudah dimodifikasi di antara pemanggilan. Kasus 2 mendemonstrasikan error ENOMEM (kode 12 di Linux) yang muncul saat malloc dipakai mengalokasikan SIZE_MAX byte—jumlah yang mustahil dipenuhi oleh sistem manapun.

Bukti Empiris: perror Mengubah errno #

Agar tidak sekadar klaim, mari kita buktikan bahwa perror memang bisa mengubah errno:

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

/* DEMONSTRASI JEBAKAN: perror() boleh mengubah errno.
   Standar C (§7.5p3) mengizinkan fungsi sukses mengubah errno.
   perror sendiri boleh mengubah errno setelah dipanggil. */
int main(void) {
    FILE *fp = fopen("/etc/xyz_tidak_ada", "r");
    if (fp == NULL) {
        int sebelum = errno;
        perror("Dengan perror dulu");
        int sesudah = errno;
        printf("errno SEBELUM perror = %d\n", sebelum);
        printf("errno SETELAH perror = %d\n", sesudah);
        if (sebelum != sesudah) {
            printf(">>> perror mengubah errno! <<<\n");
        }
    }
    return 0;
}

Output yang Dihasilkan:

text
Dengan perror dulu: No such file or directory
errno SEBELUM perror = 2
errno SETELAH perror = 22
>>> perror mengubah errno! <<<

Buktinya jelas: errno bernilai 2 (ENOENT) sebelum perror dipanggil, tetapi berubah menjadi 22 (EINVAL) setelahnya. Inilah alasan kita wajib menyimpan errno ke variabel lokal bila ingin memakainya beberapa kali.