C Erros do Sistema (errno e perror)

O fopen devolveu NULL. Mas por quê? O arquivo não existe? Existe, mas a Ana não tem permissão? O nome é de uma pasta? Para o usuário, "não consegui abrir" é pouco; "Permissão negada" diz exatamente o que fazer. Em C, quem guarda o motivo é uma variável especial chamada errno, e quem transforma o motivo em frase são o perror e o strerror. Nesta aula você vai usar os três, ver as mensagens em português e conhecer duas armadilhas que pegam até programador experiente.

Você vai aprender
  • O que é errno e quando ele recebe um valor.
  • Como mostrar o motivo de um erro com perror e montar sua própria mensagem com strerror.
  • Como fazer as mensagens saírem em português com setlocale (e por que no editor do guia elas continuam em inglês).
  • Os códigos mais comuns (ENOENT, EACCES, EISDIR, ERANGE…) e como decidir o que fazer com cada um.
  • A regra de ouro: conferir o retorno antes de olhar o errno, e guardar o errno logo depois do erro.
  • Como o strtol usa errno para avisar que o número não coube (ERANGE).

Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook, sistema em português), na pasta ~/c, com gcc -std=c23 -Wall -Wextra arquivo.c -o programa.


A ideia: o bilhete na porta

A Ana vai buscar uma encomenda na loja, e a loja está fechada. Ela sabe que não deu certo (a porta não abriu), mas não sabe por quê. Aí ela vê um bilhete colado na porta: "Fechado para almoço, volto às 14h". Agora ela sabe o que fazer.

Em C funciona assim:

  • A porta que não abre é o valor de retorno: fopen devolve NULL, fseek devolve -1, malloc devolve NULL. É isso que diz se deu errado.
  • O bilhete é o errno: um número que a função deixa para dizer por que deu errado.

E o bilhete tem uma regra importante: ninguém o tira da porta quando a loja reabre. Se você olhar o bilhete sem antes tentar a porta, pode ler um aviso velho. Isso vai ser a regra de ouro da seção 5.


1. perror: a frase pronta

O jeito mais curto de mostrar o motivo é o perror ("print error", mostrar o erro):

#include <stdio.h>

int main(void) {
    FILE *f = fopen("compras.txt", "r");
    if (f == NULL) {
        perror("compras.txt");
        return 1;
    }
    printf("Abri a lista de compras!\n");
    fclose(f);
    return 0;
}
if (f == NULL)
Primeiro, a porta: o fopen falhou? Só então vale a pena perguntar o motivo.
perror("compras.txt")
Escreve o texto que você passou, um ": ", a descrição do erro guardado em errno e uma quebra de linha. Tudo vai para a saída de erros (stderr), e não para a saída normal (aula stdin, stdout e stderr).
"compras.txt"
O texto do começo é seu. O costume é pôr o nome do arquivo ou da função que falhou, para o usuário saber o que deu errado.
return 1;
Termina com código diferente de zero: para o sistema, o programa falhou (aula exit e atexit).
$ gcc -std=c23 -Wall -Wextra abrir.c -o abrir
$ ./abrir
compras.txt: No such file or directory
$ echo $?
1
$ printf "arroz\nfeijão\n" > compras.txt          « agora o arquivo existe
$ ./abrir
Abri a lista de compras!

"No such file or directory" quer dizer "arquivo ou diretório inexistente". Mas por que em inglês, se o sistema da Ana está em português? Já explicamos na seção 3.


2. errno e strerror: o número e a frase

O perror sempre escreve no mesmo formato. Para montar sua própria mensagem, use as peças separadas:

  • errno (de "error number", número do erro), declarado em <errno.h>: um int com o código do último erro.
  • strerror(codigo) ("string error", o erro como texto), de <string.h>: devolve a frase que descreve aquele código.
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <locale.h>

int main(void) {
    setlocale(LC_ALL, "");

    char nome[100];
    printf("Arquivo: ");
    if (fgets(nome, sizeof nome, stdin) == NULL) {
        return 1;
    }
    nome[strcspn(nome, "\n")] = '\0';

    FILE *f = fopen(nome, "a");
    if (f == NULL) {
        int erro = errno;
        fprintf(stderr, "Não consegui abrir %s: %s (errno %d)\n", nome, strerror(erro), erro);
        return 1;
    }
    fprintf(f, "anotação nova\n");
    fclose(f);
    printf("Anotei em %s\n", nome);
    return 0;
}
LinhaO que faz
setlocale(LC_ALL, "")Usa o idioma do sistema nas mensagens (seção 3).
fgets + strcspnLê o nome do arquivo e tira o '\n' do fim (aula Ler uma Linha).
fopen(nome, "a")Abre para acrescentar no fim (cria se não existir).
int erro = errno;Guarda o código logo depois do erro, antes de chamar qualquer outra função (a seção 6 mostra por quê).
fprintf(stderr, …)Escreve na saída de erros, como o perror faz.
strerror(erro)A frase do código. O número também vai na mensagem, só para você ver.

Agora a Ana testa o mesmo programa em quatro situações. Antes, ela tirou a permissão de escrita da lista de compras com o comando chmod 444 (o arquivo fica "só leitura" para todos):

$ gcc -std=c23 -Wall -Wextra anotar.c -o anotar
$ chmod 444 compras.txt
$ ./anotar
Arquivo: compras.txt
Não consegui abrir compras.txt: Permissão negada (errno 13)
$ ./anotar
Arquivo: ../Documentos
Não consegui abrir ../Documentos: É um diretório (errno 21)
$ ./anotar
Arquivo: viagem/roteiro.txt
Não consegui abrir viagem/roteiro.txt: Arquivo ou diretório inexistente (errno 2)
$ chmod 644 compras.txt                    « devolve a permissão
$ ./anotar
Arquivo: compras.txt
Anotei em compras.txt

O mesmo fopen, três motivos diferentes, três mensagens que dizem ao usuário o que fazer: pedir permissão, escolher um arquivo em vez de uma pasta, criar a pasta viagem. Repare que, com "a", o fopen cria o arquivo que falta, mas não cria pastas.


3. Mensagens em português: setlocale

Todo programa em C começa no idioma neutro "C" (que, na prática, é o inglês), não importa o idioma do sistema. Para usar o idioma do usuário, chame setlocale(LC_ALL, ""), de <locale.h>, no começo do main. O "" quer dizer "o que estiver configurado no sistema" (as variáveis LANG e LC_*; veja Variáveis de Ambiente).

Este programa mostra o número e a frase de alguns códigos antes e depois do setlocale:

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

void mostrar(void) {
    printf("ENOENT  %3d %s\n", ENOENT, strerror(ENOENT));
    printf("EACCES  %3d %s\n", EACCES, strerror(EACCES));
    printf("EISDIR  %3d %s\n", EISDIR, strerror(EISDIR));
    printf("ERANGE  %3d %s\n", ERANGE, strerror(ERANGE));
}

int main(void) {
    printf("Antes do setlocale:\n");
    mostrar();
    setlocale(LC_ALL, "");
    printf("Depois do setlocale:\n");
    mostrar();
    printf("Código 0: %s; pi = %f\n", strerror(0), 3.14159);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra tabela.c -o tabela
$ ./tabela
Antes do setlocale:
ENOENT    2 No such file or directory
EACCES   13 Permission denied
EISDIR   21 Is a directory
ERANGE   34 Numerical result out of range
Depois do setlocale:
ENOENT    2 Arquivo ou diretório inexistente
EACCES   13 Permissão negada
EISDIR   21 É um diretório
ERANGE   34 Resultado numérico fora de alcance
Código 0: Sucesso; pi = 3,141590

Repare na última linha: o código 0 quer dizer "sem erro" ("Sucesso"), e o setlocale(LC_ALL, "") mudou também o jeito de escrever números: o %f passou a usar vírgula (3,141590), como se escreve no Brasil. O LC_ALL ("todas as categorias") troca mensagens, números, datas e a ordem alfabética de uma vez. Se você só quer as mensagens em português e os números com ponto, use setlocale(LC_MESSAGES, "") (do POSIX; existe no Linux e no macOS).

No editor do guia, as mensagens são sempre em inglês, mesmo com setlocale. A biblioteca C do editor (a wasi-libc, baseada na musl) não traz traduções. As frases também são um pouco diferentes das do Linux, e até os números mudam:
Códigogcc + glibc (Linux)Editor do guia
ENOENT2 · Arquivo ou diretório inexistente44 · No such file or directory
EACCES13 · Permissão negada2 · Permission denied
EINVAL22 · Argumento inválido28 · Invalid argument
ERANGE34 · Resultado numérico fora de alcance68 · Result not representable
Moral: nunca compare errno com um número escrito à mão. Use sempre os nomes (ENOENT, EACCES…), que valem em qualquer sistema.

Experimente: a caixa tem duas abas, lista.c e o arquivo compras.txt. O programa tenta abrir "compra.txt" (sem o "s") e falha: veja o número, a frase do strerror e a linha do perror. Corrija o nome no fopen e rode de novo. Depois acrescente #include <locale.h> e setlocale(LC_ALL, ""); e confira que, aqui no editor, a frase continua em inglês.

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

int main(void) {
    FILE *f = fopen("compra.txt", "r");
    if (f == NULL) {
        int erro = errno;
        printf("errno = %d\n", erro);
        printf("strerror: %s\n", strerror(erro));
        perror("compra.txt");
        return 1;
    }
    char linha[100];
    while (fgets(linha, sizeof linha, f) != NULL) {
        printf("- %s", linha);
    }
    fclose(f);
    return 0;
}
arroz
feijão
café

4. Decidir pelo código: errno == ENOENT

Nem todo erro é igual. Para a lista de compras, "o arquivo não existe" não é um problema: é só a primeira vez que a Ana usa o programa. Já "permissão negada" é um problema de verdade. Os nomes de <errno.h> deixam o programa decidir:

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

int main(void) {
    FILE *f = fopen("compras.txt", "r");
    if (f == NULL) {
        if (errno == ENOENT) {
            printf("Ainda não há lista de compras. Comece uma!\n");
            return 0;
        }
        perror("compras.txt");
        return 1;
    }

    char linha[100];
    int n = 0;
    while (fgets(linha, sizeof linha, f) != NULL) {
        n++;
        printf("%d. %s", n, linha);
    }
    fclose(f);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra lista.c -o lista
$ rm -f compras.txt
$ ./lista
Ainda não há lista de compras. Comece uma!
$ printf "arroz\nfeijão\n" > compras.txt
$ ./lista
1. arroz
2. feijão
$ chmod 000 compras.txt                    « ninguém pode ler
$ ./lista
compras.txt: Permission denied
$ chmod 644 compras.txt

Os códigos que você mais vai encontrar com arquivos e números:

NomeMensagem (glibc, em português)Quando aparece
ENOENTArquivo ou diretório inexistenteAbrir para leitura um arquivo que não existe; pasta do caminho não existe ("Error NO ENTry")
EACCESPermissão negadaSem permissão de leitura ou escrita ("Error ACCESS")
EISDIRÉ um diretórioAbrir uma pasta para escrever
ENOTDIRNão é um diretórioUm pedaço do caminho é arquivo: compras.txt/x
EEXISTArquivo existeCriar com "wx" um arquivo que já existe (C11)
ENOSPCNão há espaço disponível no dispositivoDisco cheio ao gravar
EMFILEMuitos arquivos abertosEsqueceu os fclose num laço
EINVALArgumento inválidoValor sem sentido, como fseek para posição negativa
ERANGEResultado numérico fora de alcancestrtol, strtod, pow com resultado que não cabe
EDOMArgumento numérico fora de domínioMatemática impossível, como sqrt(-1) (se a biblioteca usar errno)
ENOMEMNão foi possível alocar memóriaFalta de memória (o malloc do POSIX usa)

Só EDOM, ERANGE e EILSEQ são do padrão C. Os outros vêm do POSIX (o padrão do Linux, macOS e outros Unix) e existem em quase todo lugar, inclusive no Windows. A lista completa do Linux está em man errno.


5. A regra de ouro: retorno primeiro, errno depois

O errno tem duas manias que confundem:

  1. Uma função que dá certo não zera o errno. O bilhete velho fica na porta.
  2. Uma função que dá certo pode mudar o errno mesmo assim, por algum trabalho interno que ela tentou e não precisou.

Por isso, o errno só tem significado logo depois de uma função que avisou pelo retorno que falhou. Veja o que acontece com quem olha o errno direto:

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

int main(void) {
    FILE *velho = fopen("lista-antiga.txt", "r");   // não existe
    if (velho != NULL) {
        fclose(velho);
    }

    FILE *f = fopen("compras.txt", "r");             // existe!
    if (errno != 0) {                                // ERRADO
        perror("compras.txt");
        return 1;
    }
    printf("Abri a lista.\n");
    fclose(f);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra errado.c -o errado
$ ls compras.txt
compras.txt
$ ./errado
compras.txt: No such file or directory

O compras.txt existe e foi aberto, mas o programa diz que ele não existe. O errno ainda guardava o ENOENT do primeiro fopen, o da lista antiga. O teste certo é if (f == NULL): a porta primeiro, o bilhete depois.

FunçãoComo avisa que falhouUsa errno?
fopendevolve NULLsim (POSIX)
fclose, fflushdevolve EOFsim (POSIX)
fseekdevolve diferente de 0sim (POSIX)
ftelldevolve -1sim
fread, fwritedevolvem menos itens que o pedidodepois de ferror(f) verdadeiro (POSIX)
strtol, strtodnão têm valor especial: zere o errno antes e olhe depoissim (ERANGE)
mallocdevolve NULLno POSIX (ENOMEM)

6. Guarde o errno antes que ele mude

Qualquer função da biblioteca pode mexer no errno, inclusive as que você usa para mostrar o erro. Este programa mostra o motivo duas vezes, com perror e depois com strerror(errno):

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

int main(void) {
    FILE *f = fopen("compras.txt", "r");
    if (f == NULL) {
        perror("compras.txt");
        fprintf(stderr, "De novo: %s\n", strerror(errno));
        return 1;
    }
    fclose(f);
    return 0;
}
$ gcc -std=c23 -Wall -Wextra duas.c -o duas
$ rm compras.txt
$ ./duas
compras.txt: No such file or directory
De novo: No such file or directory
$ ./duas 2> erros.txt                « manda os erros para um arquivo
$ cat erros.txt
compras.txt: No such file or directory
De novo: Invalid argument

No terminal, as duas linhas concordam. Mas quando a saída de erros vai para um arquivo (o 2> do shell, aula stdin, stdout e stderr), o próprio perror da glibc faz um teste interno que falha e deixa EINVAL no errno. A segunda linha mostra um motivo que não tem nada a ver com o problema. Isso é real, e não é defeito da glibc: o padrão permite que qualquer função mude o errno.

A solução é a do anotar.c: copie o errno para uma variável sua na primeira linha depois do erro (int erro = errno;) e use só a cópia.


7. strtol e ERANGE: o número não coube

O strtol ("string to long", texto para long) da aula Texto ↔ Número não tem como avisar pelo retorno que o número é grande demais, porque qualquer long é um resultado possível. Então ele faz assim: devolve o maior long (LONG_MAX) ou o menor (LONG_MIN) e põe ERANGE no errno. Como funções que dão certo não zeram o errno, você zera antes:

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

int main(void) {
    setlocale(LC_ALL, "");
    const char *textos[] = {"42", "99999999999999999999", "-99999999999999999999", "12abc", "abc"};

    for (int i = 0; i < 5; i++) {
        char *fim;
        errno = 0;
        long v = strtol(textos[i], &fim, 10);

        if (fim == textos[i]) {
            printf("%-22s → não é um número\n", textos[i]);
        } else if (errno == ERANGE) {
            printf("%-22s → grande demais: %s (veio %ld)\n", textos[i], strerror(errno), v);
        } else if (*fim != '\0') {
            printf("%-22s → %ld, mas sobrou \"%s\"\n", textos[i], v, fim);
        } else {
            printf("%-22s → %ld\n", textos[i], v);
        }
    }
    return 0;
}
errno = 0;
Limpa o bilhete antes de cada conversão. Sem isso, um ERANGE da volta anterior faria o "42" parecer grande demais.
strtol(textos[i], &fim, 10)
Converte em base 10 e aponta fim para o primeiro caractere que não fez parte do número.
fim == textos[i]
Não andou nada: não havia número. Nesse caso o strtol devolve 0 e o padrão C não manda mexer no errno.
errno == ERANGE
Havia número, mas não cabia num long.
*fim != '\0'
Converteu um pedaço e sobrou texto.
$ gcc -std=c23 -Wall -Wextra converte.c -o converte
$ ./converte
42                     → 42
99999999999999999999   → grande demais: Resultado numérico fora de alcance (veio 9223372036854775807)
-99999999999999999999  → grande demais: Resultado numérico fora de alcance (veio -9223372036854775808)
12abc                  → 12, mas sobrou "abc"
abc                    → não é um número

Experimente: rode o mesmo programa aqui. O long do editor tem 4 bytes, então o "veio" mostra 2147483647, e a frase sai em inglês. Acrescente "3000000000" à lista (mude o 5 para 6): três bilhões cabem no long do Linux, mas e aqui? Depois apague a linha errno = 0; e veja se algum resultado muda.

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

int main(void) {
    setlocale(LC_ALL, "");
    const char *textos[] = {"42", "99999999999999999999", "-99999999999999999999", "12abc", "abc"};

    for (int i = 0; i < 5; i++) {
        char *fim;
        errno = 0;
        long v = strtol(textos[i], &fim, 10);

        if (fim == textos[i]) {
            printf("%-22s → não é um número\n", textos[i]);
        } else if (errno == ERANGE) {
            printf("%-22s → grande demais: %s (veio %ld)\n", textos[i], strerror(errno), v);
        } else if (*fim != '\0') {
            printf("%-22s → %ld, mas sobrou \"%s\"\n", textos[i], v, fim);
        } else {
            printf("%-22s → %ld\n", textos[i], v);
        }
    }
    return 0;
}

perror ou strerror: qual usar?

FormaQuando usar
perror("nome")O caso comum: uma linha, formato padrão nome: motivo, já vai para o stderr.
fprintf(stderr, "… %s\n", strerror(erro))Quando a mensagem precisa de mais informação (o nome que o usuário digitou, o número da linha) ou de outra ordem.
if (erro == ENOENT) …Quando o programa deve reagir diferente a cada motivo.
Por dentro: o errno parece uma variável global, mas o padrão diz que ele é uma macro que se comporta como variável. Na glibc ele vira algo como (*__errno_location()), para que cada thread do programa tenha o seu próprio errno (aula Threads). Por isso, nunca declare int errno; por conta própria: inclua <errno.h>.

Quando dá errado

O programa diz que deu erro, mas a operação funcionou

O que significa: o código testou errno != 0 no lugar do retorno, e o errno tinha um valor velho (o exemplo errado.c da seção 5).

Como consertar: teste sempre o retorno da função (f == NULL, r != 0, n < pedido). Olhe o errno só dentro desse if.

A mensagem não tem nada a ver com o problema ("Invalid argument", "Success")

O que significa: entre a função que falhou e o strerror(errno), outra função mudou o errno (um printf, um perror, um fclose). A seção 6 mostrou o perror trocando ENOENT por EINVAL quando a saída de erros vai para um arquivo. "Success" (em português, "Sucesso") é a frase do código 0: o errno foi zerado no caminho.

Como consertar: int erro = errno; na primeira linha depois do erro e, daí em diante, só erro.

error: ‘errno’ undeclared (first use in this function)

Um programa parecido com o lista.c da seção 4, mas que só tem #include <stdio.h> no topo:

$ gcc -std=c23 -Wall -Wextra semerrno.c -o semerrno
semerrno.c: In function ‘main’:
semerrno.c:6:13: error: ‘errno’ undeclared (first use in this function)
    6 |         if (errno == ENOENT) {
      |             ^~~~~
semerrno.c:2:1: note: ‘errno’ is defined in header ‘<errno.h>’; this is probably fixable by adding ‘#include <errno.h>’
    1 | #include <stdio.h>
  +++ |+#include <errno.h>
    2 |
semerrno.c:6:13: note: each undeclared identifier is reported only once for each function it appears in
    6 |         if (errno == ENOENT) {
      |             ^~~~~
semerrno.c:6:22: error: ‘ENOENT’ undeclared (first use in this function)
    6 |         if (errno == ENOENT) {
      |                      ^~~~~~

O que significa: faltou #include <errno.h>. O gcc até sugere a linha a acrescentar (o +++ |+). O perror sozinho não precisa dele (está em <stdio.h>), mas errno e os nomes ENOENT, ERANGE… precisam. No editor, o Clang diz use of undeclared identifier 'errno'.

Como consertar: acrescente #include <errno.h> no topo. Para strerror, <string.h>; para setlocale, <locale.h>.

As mensagens saem em inglês

O que significa: o programa não chamou setlocale(LC_ALL, ""), então usa o idioma neutro "C". No editor do guia, saem em inglês de qualquer jeito (não há traduções).

Como consertar: setlocale(LC_ALL, ""); no começo do main. Atenção: no Linux em português ele também faz o printf escrever 3,141590 em vez de 3.141590 (e o strtod esperar vírgula). Se isso atrapalhar, use setlocale(LC_MESSAGES, ""), que muda só as mensagens.

O strtol diz "grande demais" para um número pequeno

O que significa: o errno não foi zerado antes da conversão e guardava um ERANGE antigo.

Como consertar: errno = 0; imediatamente antes de cada strtol/strtod.


Referência rápida

EscritaO que faz
#include <errno.h>Traz errno e os nomes ENOENT, EACCES, ERANGE…
errnoCódigo do último erro; só vale logo depois de uma função que avisou que falhou
perror("texto")Escreve texto: motivo no stderr (<stdio.h>)
strerror(codigo)A frase do código (<string.h>)
int erro = errno;Guarda o código antes que outra função o mude
errno = 0;Zera antes de funções sem retorno de erro (strtol, strtod)
if (errno == ENOENT)Decide pelo motivo (sempre pelo nome, nunca pelo número)
setlocale(LC_ALL, "")Mensagens no idioma do sistema (<locale.h>)
man errnoNo terminal: a lista de códigos do Linux

Exercícios

1. A frase pronta

Complete para mostrar dados.csv: motivo quando o fopen falhar:

if (f == NULL) { ("dados.csv"); return 1; }

2. O arquivo não existe

Complete o nome do código de erro para "arquivo ou diretório inexistente":

if (errno == ) { … }

3. Antes do strtol

Complete a linha que deve vir imediatamente antes de long v = strtol(s, &fim, 10);:

4. Qual é o jeito certo de saber se o fopen falhou?

5. Por que comparar errno == 2 é má ideia?

6. O programa chama perror e, na linha seguinte, strerror(errno), e as duas mensagens são diferentes. O que aconteceu?

Missão final: criar a configuração na primeira vez

Missão: o programa lê config.txt, que não existe na primeira vez, e hoje morre com perror. No lugar marcado, faça assim: se o fopen falhou e o motivo é ENOENT, crie o arquivo com "w", grave volume=5, feche, mostre Criei config.txt com o padrão. e abra de novo para leitura. Qualquer outro erro continua indo para o perror. A saída deve ser:

Criei config.txt com o padrão.
Configuração: volume=5
#include <stdio.h>
#include <errno.h>

int main(void) {
    FILE *f = fopen("config.txt", "r");
    // se faltou o arquivo (ENOENT), crie com o padrão aqui

    if (f == NULL) {
        perror("config.txt");
        return 1;
    }

    char linha[100];
    while (fgets(linha, sizeof linha, f) != NULL) {
        printf("Configuração: %s", linha);
    }
    fclose(f);
    return 0;
}
#include <stdio.h>
#include <errno.h>

int main(void) {
    FILE *f = fopen("config.txt", "r");
    // se faltou o arquivo (ENOENT), crie com o padrão aqui
    if (f == NULL && errno == ENOENT) {
        FILE *novo = fopen("config.txt", "w");
        if (novo == NULL) {
            perror("config.txt");
            return 1;
        }
        fprintf(novo, "volume=5\n");
        fclose(novo);
        printf("Criei config.txt com o padrão.\n");
        f = fopen("config.txt", "r");
    }

    if (f == NULL) {
        perror("config.txt");
        return 1;
    }

    char linha[100];
    while (fgets(linha, sizeof linha, f) != NULL) {
        printf("Configuração: %s", linha);
    }
    fclose(f);
    return 0;
}

O && aqui é essencial: f == NULL && errno == ENOENT só olha o errno se o fopen falhou (aula Operadores Lógicos). É a regra de ouro escrita numa linha.


Resumo

  • O retorno diz se a função falhou; o errno (de <errno.h>) diz por quê. Olhe o errno só depois que o retorno avisou o erro.
  • perror("nome") mostra nome: motivo no stderr; strerror(codigo) devolve a frase para você montar sua mensagem.
  • Com setlocale(LC_ALL, ""), a glibc fala português ("Arquivo ou diretório inexistente"). No editor do guia, as frases ficam em inglês e os números dos códigos são outros.
  • Compare com os nomes (ENOENT, EACCES, EISDIR, ERANGE…), nunca com números.
  • Funções que dão certo não zeram o errno e qualquer função pode mudá-lo: guarde com int erro = errno; logo depois do erro.
  • Para strtol/strtod: errno = 0; antes, errno == ERANGE depois.