conceptC++ Moderno~4 min de lecturaActualizado 2026-07-02#cpp#errors#exceptions#expected#raii

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 factory puede devolver expected
Parser ve input de usuario inválido normalmente no
Subsistema profundo no puede completar tarea requerida a menudo sí quizás
API estilo syscall o kernel devuelve status tipo errno normalmente no
Hot path donde la falla es común normalmente no
Proyecto compila con -fno-exceptions no
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.
  • expected puede 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, int o 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.
  • noexcept es un contrato. Marcar una función noexcept por 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