Por qué existen las excepciones
En la página anterior usaste enum class para dar nombres significativos a los estados de error. Eso está muy bien para resultados que una función espera y que se supone que quien la llama debe inspeccionar. Pero algunos fallos son diferentes: una función en lo más profundo de tu pila de llamadas descubre que un archivo no abre, o que un argumento no tiene sentido, y no tiene ni idea de qué debería hacer el programa al respecto. Devolver un código de error solo funciona si cada llamante de la cadena se acuerda de comprobarlo y propagarlo hacia arriba. Si te saltas una sola comprobación, el programa sigue navegando con basura.
Las excepciones resuelven esto. Cuando algo va mal, lanzas un objeto con throw. La ejecución se detiene de inmediato, la pila se desenrolla (cada objeto local entre el throw y el manejador ejecuta su destructor) y el control salta al catch coincidente más cercano. Una excepción no manejada no se puede ignorar en silencio: si nada la captura, el programa llama a std::terminate y aborta.
Esta página se centra en el lado del lanzamiento: los propios objetos de error. La siguiente página profundiza en la maquinaria de try/catch en detalle.
El lanzamiento y el mensaje de what()
Técnicamente puedes lanzar (throw) cualquier valor —throw 42; o throw "oops"; son legales— pero no lo hagas. La convención que todo el mundo sigue es lanzar un objeto derivado de std::exception. Esa clase base declara un único método virtual, what(), que devuelve una descripción const char* del problema. Atenerse a la convención significa que un solo catch (const std::exception& e) puede manejar cualquier cosa.
El encabezado <stdexcept> te da tipos ya hechos cuyo constructor recibe el mensaje:
Fíjate en que what() devuelve exactamente la cadena con la que construiste la excepción. Fíjate también en que la capturamos por const exception& aunque lanzamos un runtime_error: eso funciona porque runtime_error es un std::exception (una relación que reconocerás de la página sobre herencia).
La jerarquía de excepciones estándar
Antes de escribir tu propio tipo de excepción, comprueba si la biblioteca estándar ya tiene uno que encaje. Todos heredan de std::exception y se dividen en dos familias en <stdexcept>:
logic_error— un fallo en la lógica del programa que podría, en principio, detectarse antes de ejecutar. Sus subtipos incluyeninvalid_argument,out_of_range,domain_errorylength_error.runtime_error— un fallo que solo aparece en tiempo de ejecución y que no es un error de programación en sí mismo. Sus subtipos incluyenrange_error,overflow_erroryunderflow_error.
Muchas funciones de biblioteca lanzan estas excepciones por ti. Por ejemplo, std::vector::at() comprueba los límites y lanza out_of_range en lugar de dejarte leer más allá del final:
Ese at() es la contraparte segura de v[9]. El operator[] simple no comprueba los límites: leer v[9] aquí es comportamiento indefinido, no una excepción. Elegir at() es la forma de convertir una corrupción silenciosa en un error capturable.
Elige el tipo que describe el error: invalid_argument cuando quien llama pasa algo sin sentido, out_of_range para problemas de índice/clave, runtime_error para "el mundo exterior me ha fallado".
Escribir tu propio tipo de excepción
Cuando ningún tipo estándar encaja —quieres adjuntar datos extra, o capturar (catch) específicamente tu error y nada más— define una clase que herede de std::exception (o de uno de sus subtipos) y sobrescribe what(). Heredar de std::runtime_error es la vía más fácil porque ya almacena el mensaje e implementa what() por ti:
Como NetworkError lleva un código de estado, el manejador puede reaccionar a él: reintentar ante un 5xx, rendirse ante un 4xx. Una simple cadena de error no podría hacerlo. El tipo personalizado también permite que un catch (const NetworkError&) capture solo los problemas de red y deje todo lo demás al manejador más general que tiene debajo.
Si alguna vez heredas directamente de std::exception (no de runtime_error), recuerda sobrescribir what() tú mismo y marcarlo como noexcept para que coincida con la firma de la base:
class ParseError : public std::exception {
public:
const char* what() const noexcept override {
return "failed to parse input";
}
};
Lanza por valor, captura por referencia
Esta es la regla más importante de las excepciones en C++, y la que los principiantes hacen mal. Lanza los objetos por valor y captúralos por referencia const.
throw runtime_error("oops"); // por valor - correcto
catch (const runtime_error& e) { ... } // por referencia const - correcto
Capturar por valor en su lugar —catch (std::exception e)— copia la excepción en un objeto de la clase base y recorta la parte derivada. Tras el recorte, e.what() llama a la implementación de la base, no a la tuya sobrescrita, así que tu mensaje cuidadosamente elaborado desaparece:
try {
throw NetworkError(503, "service unavailable");
} catch (std::exception e) { // por valor - ¡recorte de objeto!
std::cout << e.what(); // mensaje genérico, status() ha desaparecido
}
La referencia (&) preserva el tipo dinámico real, de modo que el what() virtual se despacha correctamente y todavía puedes acceder a los miembros derivados. Añade const porque solo estás leyendo la excepción, no modificándola. Nunca lances un puntero (throw new runtime_error(...)): quien la capture tendría que hacerle delete, ¿y en qué ruta de código? Esa es exactamente la fuga que las excepciones deberían evitar.
Siguiente: try-catch
Ahora ya sabes crear y lanzar (throw) excepciones bien formadas y elegir el tipo estándar adecuado para cada fallo. La otra mitad de la historia es el lado de la captura. La siguiente página cubre try/catch por completo: ordenar varios bloques catch de lo más específico a lo más general, el atrapatodo catch (...), el relanzamiento con un throw; desnudo y cómo RAII (recuerda los punteros inteligentes) garantiza que tus recursos se liberen mientras la pila se desenrolla.
Preguntas frecuentes
¿Qué es una excepción en C++?
Una excepción es un objeto que señala un error que la función actual no puede manejar por sí sola. Tú la lanzas con throw, la pila se desenrolla (destruyendo los objetos locales por el camino) y un bloque catch que coincida más arriba toma el control. Separa el código que detecta un problema del código que decide qué hacer al respecto.
¿Cuál es la diferencia entre throw y return para los errores?
Un valor de return tiene que ser comprobado por quien llama, y es fácil olvidarlo: el programa simplemente sigue adelante con datos erróneos. Una excepción lanzada no se puede ignorar: si nadie la captura, el programa termina. Las excepciones son para fallos genuinos (un archivo no abre, la entrada no es válida); los valores de retorno siguen siendo lo correcto para resultados ordinarios, incluidos los casos esperados de "no encontrado".
¿Qué hace el método what() en las excepciones de C++?
Toda clase derivada de std::exception proporciona un método virtual what() que devuelve un const char* que describe el error. Cuando capturas una excepción, llamar a e.what() te da el mensaje legible que puedes registrar o imprimir. Los tipos de excepción estándar lo establecen a partir de la cadena que pasas a su constructor.