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 -> DCada 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 encontradoo
cliente inexistentey 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::nulloptno 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
↓
DSi 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 corruptaExcepciones
Cuando ocurre algo excepcional o inesperado.
Ejemplos:
memoria insuficiente
archivo corrupto
invariante rotao 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.