Manejo de errores en C++

498 palabras 3 minutos C++

Una de las decisiones más importantes al diseñar una aplicación en C++ es cómo manejar los errores. El lenguaje ofrece varias alternativas, cada una con ventajas, desventajas y casos de uso específicos.

En este artículo vamos a comparar:

  • Códigos de retorno
  • Parámetros de salida
  • std::optional
  • std::expected
  • Excepciones

1. Códigos de retorno

La técnica más antigua, heredada del lenguaje C.

bool abrirArchivo(const std::string& nombre)
{
    ...
    return false;
}

Uso:

if (!abrirArchivo("datos.txt"))
{
    // manejar error
}

También es común devolver un entero:

int abrirArchivo(...);

donde:

0  = éxito
-1 = archivo inexistente
-2 = permiso denegado

Ventajas

  • Muy simple.
  • Muy eficiente.
  • Compatible con cualquier compilador.
  • No utiliza excepciones.

Desventajas

  • Es fácil olvidar verificar errores.
abrirArchivo("datos.txt"); // nadie verificó el resultado
  • Los errores se propagan mal.
A -> B -> C -> D

Cada función debe verificar y reenviar el error manualmente.


2. Parámetro de salida + código de retorno

Muy común en APIs antiguas.

bool dividir(
    int a,
    int b,
    int& resultado)
{
    if (b == 0)
        return false;

    resultado = a / b;
    return true;
}

Uso:

int r;

if (dividir(10, 2, r))
{
    ...
}

Ventajas

  • Muy eficiente.
  • No requiere asignaciones adicionales.
  • Fácil de implementar.

Desventajas

  • Las firmas son menos claras.
  • Se vuelve incómodo cuando hay múltiples valores de salida.

3. std::optional

Disponible desde C++17.

Representa un valor que puede existir o no existir.

std::optional<Usuario>
buscarUsuario(int id);

Uso:

auto u = buscarUsuario(5);

if (u)
{
    ...
}

¿Qué representa?

Una ausencia válida de resultado.

Por ejemplo:

buscarCliente()

puede devolver:

cliente encontrado

o

cliente inexistente

y ambas situaciones son normales.

Ventajas

  • Muy expresivo.
  • Fácil de usar.
  • El compilador obliga a considerar la ausencia de valor.

Desventajas

No informa la causa.

std::nullopt

no indica si:

  • faltó el archivo
  • hubo un problema de permisos
  • falló la base de datos

4. std::expected

Introducido en C++23.

Permite devolver un resultado o un error tipado.

std::expected<Usuario, Error>
buscarUsuario(int id);

Uso:

auto r = buscarUsuario(5);

if (!r)
{
    std::cout << r.error().message;
}

Ventajas

  • Explícito.
  • Tipado.
  • No utiliza excepciones.
  • Obliga a manejar errores.

Desventajas

La propagación sigue siendo manual.

auto r = cargar();

if (!r)
    return std::unexpected(r.error());

5. Excepciones

La técnica más conocida de C++.

throw std::runtime_error("error");

Captura:

try
{
    ...
}
catch(const std::exception& e)
{
    ...
}

Ventajas

Los errores se propagan automáticamente.

A

B

C

D

Si D falla:

throw;

el error puede llegar directamente hasta A.

No es necesario escribir:

if (!resultado)

en cada nivel.

Desventajas

El flujo de errores no es visible en la firma de la función.

guardar();

podría lanzar una excepción y no ser evidente al leer la interfaz.


Comparación rápida

MétodoTiene valorTiene error detalladoObliga a verificarPropaga solo
Return codeA vecesNoNo
Out parameterPocoNoNo
optionalNoNo
expectedNo
ExcepcionesNo

¿Cuándo usar cada uno?

std::optional

Cuando la ausencia de un resultado es normal.

buscarCliente()

Resultado posible:

lo encontré
no lo encontré

sin que exista un error.


std::expected

Cuando una operación puede fallar por razones conocidas.

abrirBaseDatos()

Posibles resultados:

ok
archivo inexistente
permiso denegado
base corrupta

Excepciones

Cuando ocurre algo excepcional o inesperado.

Ejemplos:

memoria insuficiente
archivo corrupto
invariante rota

o cuando no tiene sentido que cada función intermedia maneje el error.


Recomendación para aplicaciones de escritorio

Para una aplicación con wxWidgets y SQLite, una estrategia equilibrada puede ser:

Core

Utilizar std::expected.

Result<Cuenta>
ObtenerCuenta(int id);

Interfaz gráfica

Mostrar mensajes al usuario cuando se recibe un error.

wxMessageBox(...)

Errores fatales

Capturar excepciones globalmente.

catch(...)
{
    LogError(...);
    wxMessageBox(...);
}

Conclusión

No existe una única estrategia correcta para todos los casos.

Una combinación práctica para aplicaciones modernas suele ser:

  • std::optional para ausencia normal de datos.
  • std::expected para errores de negocio y acceso a datos.
  • Excepciones para errores fatales o inesperados.
  • Códigos de retorno únicamente en APIs de bajo nivel o por compatibilidad.

Elegir la herramienta adecuada para cada situación produce código más claro, mantenible y robusto.