Error handling: exceptions vs expected
Las exceptions son transferencia no-local de falla: una función dice "no pude completar mi
contrato" y el control salta a un handler compatible mientras destructores limpian los
scopes unwinded. Los valores estilo expected son datos de falla locales: una función
devuelve un valor o un error, y los callers deciden inmediatamente cómo seguir. Ninguno es
la respuesta universal de C++. La elección correcta depende de si la falla es excepcional,
si la recuperación es local, si los constructors necesitan fallar y si tu codebase permite
maquinaria de exceptions.
El reset: error handling es parte de la API. No elijas solo por gusto. Elegí por la forma de la falla y por el runtime que tu programa puede cargar.
Cómo funciona de verdad
Con exceptions, un throw transfiere control hacia arriba en el call stack hasta un
catch compatible. Los objetos cuyos lifetimes terminan durante stack unwinding ejecutan
sus destructores, por eso RAII y exception safety son inseparables. Las garantías comunes
son nothrow, strong, basic y no guarantee. Un destructor no debería throwear durante
unwinding, y noexcept significa que una excepción que escapa llama a std::terminate.
Con std::expected<T, E>, estandarizado en C++23, el objeto de retorno contiene un T o un
E. El caller puede testearlo, inspeccionar el valor, inspeccionar el error o componerlo
con operaciones monádicas cuando la implementación las provee. El camino de falla es
visible en la firma de la función, pero también puede volverse ruidoso si cada capa solo
forwardea errores mecánicamente.
| Forma | Exceptions encajan | expected encaja |
|---|---|---|
| Constructor no puede establecer invariant | sí | factory puede devolver expected |
| Parser ve input de usuario inválido | normalmente no | sí |
| Subsistema profundo no puede completar tarea requerida | a menudo sí | quizás |
| API estilo syscall o kernel devuelve status tipo errno | normalmente no | sí |
| Hot path donde la falla es común | normalmente no | sí |
Proyecto compila con -fno-exceptions |
no | sí |
| Borde de library con política desconocida | riesgoso | explícito |
Hay una tercera categoría: bugs y preconditions violadas. Eso suele ser assertions,
sanitizers, tests o terminación del proceso, no errores recoverable. Devolver expected
desde una función llamada con argumentos imposibles puede entrenar a callers a recuperarse
de invariantes corruptas.
Artefacto ejecutable: throwing parse y parse estilo expected
El demo ejecutable vive en examples/modern-cpp/error-handling-exceptions-vs-expected/.
Compila como C++20, así que usa un Expected<T, E> local mínimo construido sobre
std::variant en vez de exigir std::expected de C++23.
cd examples/modern-cpp/error-handling-exceptions-vs-expected
./run.sh
El camino throwing usa std::stoi y convierte fallas de parseo en exceptions:
static int parse_or_throw(std::string_view text)
{
if (text.empty())
throw std::invalid_argument("empty");
std::string owned { text };
std::size_t consumed = 0;
int value = std::stoi(owned, &consumed, 10);
if (consumed != owned.size())
throw std::invalid_argument("trailing characters");
return value;
}
El camino explícito usa std::from_chars, que no aloca ni throwea:
static ParseResult parse_expected(std::string_view text)
{
if (text.empty())
return ParseError::Empty;
int value = 0;
auto const* first = text.data();
auto const* last = text.data() + text.size();
auto [position, error] = std::from_chars(first, last, value);
if (error == std::errc::result_out_of_range)
return ParseError::Overflow;
if (error == std::errc::invalid_argument || position != last)
return ParseError::Invalid;
return value;
}
El script emite demo.O2.s. Compará el camino normal y el camino de falla. El parseo
explícito son branches ordinarios y estado retornado. El parseo throwing tiene un camino de
control separado y depende del runtime de exceptions y unwind tables que usen tu ABI y
compiler flags.
Fallas típicas y trade-offs
- Exceptions sin RAII leakéan. Si el cleanup es manual, unwinding saltea el cleanup code que no envolviste en un destructor.
- Catching demasiado amplio esconde invariantes.
catch (...)en una capa baja puede convertir un bug de programación en una condición fake recoverable. - Throwear por errores normales de input vuelve sorprendente el control flow. CLI input inválido, paquetes mal formados y cache misses suelen ser fallas esperadas.
expectedpuede crear túneles de boilerplate. Una pila de funciones que solo chequea y forwardea errores puede oscurecer el success path.- Los tipos de error pueden ser demasiado débiles.
bool,into un error solo string muchas veces no preservan lo que el caller necesita para recuperarse. - Mezclar políticas es caro. Un borde library sin exceptions, una dependency third-party throwing y una API kernel-style errno necesitan adapters, no deseo.
noexceptes un contrato. Marcar una funciónnoexceptpor velocidad y después dejar que escape una excepción convierte recovery en termination.
En la práctica
Usá exceptions cuando una función no puede cumplir su contrato, la recuperación vive
naturalmente en una capa más alta, constructors necesitan fallar limpiamente y el codebase
ya es exception-safe. Usá expected o un equivalente de proyecto cuando la falla es un
resultado ordinario, el caller debe branchear localmente o el runtime prohíbe exceptions.
Usá assertions para suposiciones internas violadas.
Para low-level code, sé especialmente estricto en los bordes. Syscalls, drivers, allocators, parsers y protocol code suelen beneficiarse de valores de error explícitos. Hosted application code puede usar exceptions efectivamente, pero solo si el proyecto diseña consistentemente para exception safety.
Conecta con: RAII: la idea que cambia todo · Move semantics y value categories · Modern C++ highlights que vale la pena adoptar · Qué subset de C++ usar · Undefined behavior: el contrato · Sanitizers: ASan, UBSan y TSan · El syscall: cruzar al kernel
Fuentes
- cppreference - Exceptions - modelo de lenguaje para throw/catch, unwinding y exception-safety guarantees. https://en.cppreference.com/w/cpp/language/exceptions
- cppreference -
std::expected- vocabulary type value-or-error de C++23 y operaciones observer. https://en.cppreference.com/w/cpp/utility/expected - cppreference -
std::from_chars- API de parseo numérico no-allocating y non-throwing usada por el demo. https://en.cppreference.com/cpp/utility/from_chars - C++ Core Guidelines - reglas de error handling y RAII que vuelven usables las exceptions en vez de leak-prone. https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
- Google C++ Style Guide - Exceptions - política pragmática no-exceptions y rationale desde un codebase grande existente. https://google.github.io/styleguide/cppguide.html
- ISO C++ FAQ - Exceptions - discusión de diseño más larga sobre cuándo son apropiadas las exceptions en C++. https://isocpp.org/wiki/faq/exceptions
- LLVM Coding Standards - ejemplo de proyecto systems importante que evita exceptions y RTTI por razones de tamaño y política. https://llvm.org/docs/CodingStandards.html