Menu

try-catch en C++: maneja excepciones de la forma correcta

Envuelve el código arriesgado en try y reacciona en catch. Aprende a capturar excepciones por referencia constante, ordenar varios manejadores, usar catch (...) y relanzar, sin perder recursos.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

De lanzar a manejar

En la página anterior aprendiste a throw (lanzar) una excepción cuando algo sale mal. Lanzar es solo la mitad de la historia: una excepción que nunca se captura llama a std::terminate y hace que tu programa se bloquee. La instrucción try/catch es la forma de manejar lo que se lanzó y seguir ejecutando.

La estructura es sencilla: pon el código arriesgado dentro de un bloque try y haz que le sigan uno o más bloques catch que reaccionen a tipos de error específicos. Si el bloque try se ejecuta sin problemas, se omite cada catch. En el momento en que algo lanza una excepción, el control salta directamente al primer catch que coincida.

Fíjate en que "after" nunca se imprime. En cuanto se dispara el throw, se abandona el resto del bloque try y la ejecución se reanuda dentro del catch que coincide. Una vez que termina el catch, el programa continúa normalmente debajo de él.

Captura por referencia constante

El hábito más importante en el manejo de errores de C++: captura las excepciones por referencia const, no por valor.

Capturar por valor copia la excepción y, peor aún, la recorta (slicing). Las excepciones estándar forman una jerarquía (runtime_error y logic_error derivan ambas de std::exception), por lo que capturar una excepción derivada como un valor base elimina la parte derivada. Capturar por referencia mantiene el objeto intacto y polimórfico:

Aquí lanzamos un out_of_range pero lo capturamos como const exception&. Como out_of_range deriva de exception, el manejador de la clase base coincide, y la referencia hace que e.what() siga devolviendo el mensaje real. Si hubieras escrito catch (exception e) (por valor), el objeto se recortaría a un simple exception y podrías perder el mensaje específico.

Varios bloques catch

A un solo try pueden seguirle varios bloques catch, cada uno para un tipo de excepción distinto. C++ los prueba de arriba abajo y ejecuta el primero que coincida, así que ordénalos del más específico al más general.

Como invalid_argument es más específico que exception, debe ir primero. Si invirtieras el orden y pusieras catch (const exception&) arriba, este absorbería todas las excepciones: el manejador de invalid_argument que está debajo se convertiría en código muerto que nunca se ejecutaría. Muchos compiladores advierten sobre esto, pero el lenguaje no te lo impide.

catch (...) y relanzamiento

A veces quieres una red de seguridad para cualquier cosa que no anticipaste. El manejador genérico catch (...) coincide con todo tipo de excepción, incluidas las que no derivan de std::exception (alguien puede escribir throw 42; o throw "oops";).

El inconveniente es que no obtienes ningún objeto: no hay ningún e que inspeccionar. Por eso catch (...) se usa mejor como último recurso: registrar que algo falló, o limpiar y relanzar.

Para relanzar la excepción actual (pasarla a un manejador externo después de hacer alguna limpieza o registro local) usa un throw; sin operando. Esto preserva la excepción original (su tipo y mensaje reales), a diferencia de throw e;, que relanzaría una copia recortada:

El manejador interno registra y relanza; el manejador externo en main se encarga entonces de ella. Usa un throw; sin operando para esto, nunca throw e;.

Desenrollado de la pila y RAII

Cuando una excepción se propaga fuera de un bloque try, C++ realiza el desenrollado de la pila (stack unwinding): a cada objeto local entre el throw y el catch que coincide se le llama su destructor, en orden inverso al de construcción. Esto es lo que hace que las excepciones sean seguras: los recursos que mantienen los objetos de la pila se liberan automáticamente.

Esta es exactamente la razón por la que deberías mantener los recursos en tipos RAII (como std::vector, std::string y los punteros inteligentes) en lugar de usar new/delete manuales. Mira lo que ocurre cuando una excepción atraviesa una asignación manual:

void leaky() {
    int* buffer = new int[1000];
    mightThrow();        // si esto lanza, la siguiente línea nunca se ejecuta...
    delete[] buffer;     // ...y el buffer se filtra
}

Como el throw salta por encima de delete[], la memoria se pierde. Un puntero inteligente lo soluciona gratis: su destructor se ejecuta durante el desenrollado:

void safe() {
    auto buffer = std::make_unique<int[]>(1000);
    mightThrow();   // si esto lanza, el destructor de buffer libera igualmente la memoria
}                   // sin delete manual, sin fugas, incluso por la ruta de la excepción

La conclusión: no intentes capturar una excepción solo para hacer delete de algo. Deja que los destructores se encarguen de la limpieza y reserva el catch para decidir cómo recuperarte.

Errores comunes y trampas

Un puñado de trampas aparecen una y otra vez:

No uses excepciones para el flujo de control normal. Lanzar y desenrollar es mucho más lento que un simple if. Reserva las excepciones para condiciones de error genuinamente excepcionales, no para "el usuario escribió una cadena vacía".

Un bloque catch vacío oculta errores. Escribir catch (...) {} para silenciar un error hace que los fallos desaparezcan sin dejar rastro. Como mínimo, registra el problema; por lo general deberías relanzarlo o manejarlo correctamente.

Un destructor que lanza es peligroso. Si un destructor lanza una excepción durante el desenrollado de la pila (mientras otra excepción ya está en curso), el programa llama a std::terminate. En el C++ moderno los destructores son implícitamente noexcept: nunca dejes que una excepción escape de uno.

El catch solo ve lo que el try cubre. Una excepción lanzada antes de entrar en el try, o en una función distinta que no está en la ruta de llamadas dentro de él, no se capturará aquí. El catch solo protege el código que se ejecuta dentro de su propio bloque try (directamente o en las funciones que llama).

Siguiente: comportamiento indefinido

Las excepciones son la forma definida en que C++ te dice que algo salió mal: lanzas, capturas, el comportamiento es predecible. Pero C++ también tiene un rincón más oscuro donde el lenguaje no hace ninguna promesa: desreferenciar un puntero colgante, leer más allá del final de un array, el desbordamiento de enteros con signo. La siguiente página cubre el comportamiento indefinido: qué lo desencadena, por qué puede parecer que "funciona" justo hasta que deja de hacerlo de forma catastrófica, y cómo mantenerlo fuera de tu código.

Preguntas frecuentes

¿Cómo funciona try-catch en C++?

Colocas el código que podría lanzar una excepción dentro de un bloque try { }. Si se lanza una excepción, el programa deja de ejecutar el resto del bloque try y salta al primer bloque catch que coincida, donde manejas el error. Si no se lanza nada, los bloques catch se omiten por completo.

¿Por qué deberías capturar las excepciones por referencia constante en C++?

Capturar por referencia (catch (const std::exception& e)) evita copiar el objeto de la excepción y, lo más importante, preserva el polimorfismo, de modo que una excepción derivada capturada como su tipo base sigue llamando al what() correcto. Capturar por valor (catch (std::exception e)) recorta la parte derivada y puede perder información.

¿Cómo se captura cualquier excepción en C++?

Usa catch (...): los puntos suspensivos capturan cualquier excepción sin importar su tipo. Es un manejador útil como último recurso, pero como no obtienes ningún objeto que inspeccionar, colócalo después de tus bloques catch específicos y úsalo principalmente para registrar el error o relanzarlo.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR