Manejo de errores con errno y valores de retorno en C

En C, no existen las excepciones como en otros lenguajes (donde un error lanza un objeto que “salta” hacia un bloque catch). En su lugar, el lenguaje utiliza una convención de valores de retorno combinada con una variable global llamada errno.

Cuando una función de la biblioteca estándar (como fopen para archivos o malloc para memoria) falla, no puede lanzar una interrupción, así que lo que hace es devolver un valor especial que indica el fallo: si la función devuelve un puntero, devolverá NULL si algo salió mal; si devuelve un entero (como un índice o un código de estado), devolverá -1.

Sin embargo, el valor de retorno solo te dice que hubo un error, pero no el porqué. Para eso está errno. errno es un número entero definido en <errno.h> que actúa como un código de estado. Para que sea útil, las funciones deben seguir una regla de oro: solo deben modificar errno si su valor de retorno indica un error. Si una función tiene éxito, no debe tocar errno.

Esto funciona de esta manera para mantener el lenguaje ligero y eficiente. Implementar un sistema de excepciones requeriría una infraestructura de ejecución (runtime) compleja que ralentizaría cada operación. En su lugar, se delega la responsabilidad al programador. Debes usar esta convención siempre que trabajes con entradas/salidas, gestión de memoria o llamadas al sistema. Si ignoras el valor de retorno y procedes a usar un puntero que es NULL o un número que debería ser un índice pero es -1, el programa intentará acceder a memoria inválida y sufrirá un crash (segmentation fault), o peor aún, continuará con datos corruptos provocando un comportamiento impredecible.

Fíjate en un detalle crítico: errno es “persistente” (o sticky). Si una función falla y establece errno en un código de error, ese valor se quedará ahí incluso si llamas a otra función después. Por eso, la norma es resetear errno = 0 justo antes de llamar a una función que sospechas que puede fallar. En sistemas modernos con hilos (threads), errno no es una variable global única, sino que es thread-local; esto significa que cada hilo de ejecución tiene su propia copia de errno para que un hilo no sobrescriba el error de otro.

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

int main(void) {
    /* Caso 1: Error al abrir un archivo. 
       fopen devuelve NULL si no puede abrir el archivo. */
    errno = 0; // Resetear antes de la operación
    FILE *fp = fopen("archivo_inexistente.txt", "r");

    if (fp == NULL) {
        /* perror imprime el mensaje personalizado, seguido de ":" 
           y la descripción de errno (ej: "archivo_inexistente.txt: No such file or directory") */
        perror("Error crítico al abrir el archivo");
    } else {
        fclose(fp);
    }

    /* Caso 2: Error en conversión de strings.
       strtol (string to long) permite detectar errores de conversión mediante errno. */
    char *buffer = "no_es_un_numero";
    char *endptr;
    errno = 0; // Resetear para asegurar que estamos viendo un error nuevo
    
    long valor = strtol(buffer, &endptr, 10);

    if (*endptr != '\0') {
        /* strerror convierte el código numérico de errno en un string legible */
        printf("Error en la conversión: %s\n", strerror(errno));
    } else {
        printf("Valor convertido: %ld\n", valor);
    }

    /* Caso 3: Error de memoria.
       malloc devuelve NULL si no hay memoria suficiente. */
    errno = 0;
    int *datos = malloc(0xFFFFFFFFUL); // Intentar asignar una cantidad irrazonable

    if (datos == NULL) {
        printf("Fallo de memoria detectado: %s\n", strerror(errno));
    }

    return 0;
}

Desglose del código

En el primer caso, fopen intenta abrir un archivo que no existe. Al fallar, la función devuelve NULL. Como la función detectó el error, el sistema operativo establece errno con un código que representa “archivo no encontrado”. Al llamar a perror, el programa lee ese código y lo traduce automáticamente en un mensaje legible para el usuario, facilitando el debugging.

En el segundo ejemplo, usamos strtol. A diferencia de la función antigua atoi, strtol es más robusta y es capaz de informar si la conversión falló debido a un problema de formato. Al resetear errno = 0 al principio, nos aseguramos de que si strtol no modifica el valor, el error que veamos no sea un “residuo” de una función llamada anteriormente.

En el tercer caso, malloc falla al recibir un tamaño excesivo. Aquí, el valor de retorno es NULL. Al usar strerror(errno), obtenemos la descripción textual del error (como “Cannot allocate memory”) de forma manual, lo cual es útil cuando quieres formatear tu propio mensaje de error sin usar perror.

El error frecuente

Un error muy común es intentar comprobar errno sin haber verificado primero el valor de retorno de la función.

// ANTIPATRÓN: ERROR GRAVE
int resultado = funcion_que_puede_fallar();

// No se ha verificado 'resultado' primero.
// Si 'resultado' fue exitoso, pero errno ya tenía un valor de 
// una función llamada hace 10 minutos, entrarás en el error por error.
if (errno != 0) { 
    printf("Error detectado\n");
}

errno es una variable que mantiene su estado hasta que otra función la cambie. Si llamas a una función exitosa, errno no se resetea a cero automáticamente. Por tanto, la única forma segura de usar errno es:
1. Resetear errno = 0.
2. Llamar a la función.
3. Comprobar si el valor de retorno indica error (como -1 o NULL).
4. Solo entonces, examinar errno para saber qué pasó.

Si usas herramientas como AddressSanitizer o Valgrind, estas te ayudarán a detectar si estás intentando usar un puntero que es NULL (causado por un error de malloc no verificado), que es la consecuencia más común de ignorar el manejo de errores.

88

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio