Manejo de errores en 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::optionalstd::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 denegadoVentajas
- 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étodo | Tiene valor | Tiene error detallado | Obliga a verificar | Propaga solo |
|---|---|---|---|---|
| Return code | Sí | A veces | No | No |
| Out parameter | Sí | Poco | No | No |
| optional | Sí | No | Sí | No |
| expected | Sí | Sí | Sí | No |
| Excepciones | Sí | Sí | No | Sí |
¿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::optionalpara ausencia normal de datos.std::expectedpara 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.