Abre un archivo que no existe en Python y open() lanza un FileNotFoundError que sube por la pila de llamadas hasta salir de tu funci贸n. Haz lo mismo en Go y os.ReadFile te devuelve un valor error junto a un slice de bytes nil. El mismo fallo, dos formas de c贸digo completamente distintas a su alrededor. La diferencia no es cosm茅tica: decide si quien llama puede ignorar el fallo por accidente, y si el compilador se da cuenta cuando lo hace.
驴Qu茅 hacen realmente distinto las excepciones y los valores de error?
Una excepci贸n es control de flujo que salta. Algo falla, la funci贸n lanza, y la ejecuci贸n abandona la pila de llamadas actual para aterrizar en el primer bloque catch o except que coincida, sin importar cu谩ntos niveles m谩s arriba est茅. La firma de la funci贸n no da ninguna pista de que esto pueda pasar. loadConfig(path: string): Config en TypeScript parece exactamente una funci贸n que no puede fallar, hasta que lanza una excepci贸n por un archivo que falta.
Un valor de error es un dato. La funci贸n lo devuelve junto al resultado que se le pidi贸, o en su lugar. El (Config, error) de Go, el Result<Config, ConfigError> de Rust, y una b煤squeda que devuelve null cuando no encuentra nada son la misma idea: el fallo forma parte del tipo de retorno, as铆 que quien llama lo ve directamente en el punto de la llamada. En Rust el sistema de tipos te obliga a tratarlo antes de tocar el resultado. En Go lo hace la convenci贸n.
Ninguno de los dos enfoques es nuevo, y ninguno ha ganado. C devolv铆a valores centinela como -1 y NULL; las excepciones llegaron despu茅s, para sacar el manejo de errores de la l贸gica principal. Python, Java, C# y JavaScript usan excepciones por defecto. Go y Rust usan valores de error por defecto. La mayor铆a de los lenguajes que adoptan un enfoque por defecto te deja recurrir al otro cuando encaja mejor.
驴C贸mo devuelve Go los errores como valores?
Go no tiene excepciones para fallos ordinarios. Una funci贸n que puede fallar devuelve un error como 煤ltimo valor, y quien la llama lo comprueba de inmediato:
| |
%w envuelve el error original en lugar de descartarlo, as铆 errors.Is y errors.As m谩s arriba en la pila pueden comprobar qu茅 sali贸 mal sin comparar cadenas de texto en el mensaje.
El punto d茅bil: nada te impide escribir data, _ := os.ReadFile(path) y descartar el error. El compilador lo acepta sin problema. Un linter como errcheck detecta los errores sin comprobar, pero solo si est谩 configurado en tu CI.
驴C贸mo saltan las excepciones de Python a un manejador?
El mismo cargador de configuraci贸n en Python no tiene comprobaciones de error en l铆nea despu茅s de cada llamada. La comprobaci贸n ocurre alrededor de la llamada, no dentro de ella:
| |
raise ... from e mantiene el traceback original enlazado al nuevo ConfigError, igual que %w en Go mantiene accesible el error original.
La diferencia que importa es la firma. load_config nunca dice que puede lanzar una excepci贸n. Quien la llama tres niveles m谩s arriba, si olvid贸 el try, lo descubre cuando el programa se cae con un traceback en producci贸n, porque nada en el lenguaje comprueba el punto de la llamada.
驴C贸mo obliga Result de Rust a manejar el error?
Rust mantiene la idea del error como valor y a帽ade un sistema de tipos que no te deja tocar el valor de 茅xito sin reconocer el caso de fallo:
| |
Esto necesita serde (con la feature derive) y serde_json en el Cargo.toml.
Result<T, E> es un enum con dos variantes, Ok(T) y Err(E), y no puedes sacar una Config sin manejar ambas. El operador ? es un atajo para ese match: si fs::read_to_string devuelve Err, load_config retorna de inmediato con ese error, mapeado a ConfigError::Io. Si no, desempaqueta el Ok y sigue.
A煤n puedes saltarte todo esto con .unwrap(), que entra en panic con Err. Es la versi贸n de Rust de una excepci贸n sin capturar. Tr谩talo como algo que cuestionar en una revisi贸n de c贸digo, no como un atajo.
驴Por qu茅 fetch() no lanza una excepci贸n en un 404?
Esta divisi贸n no respeta las fronteras entre lenguajes: una misma API puede mezclar ambos modelos para fallos distintos. fetch() rechaza su promise ante un fallo de red (el DNS no resuelve, la conexi贸n se rechaza) pero se resuelve con normalidad ante un 404 o un 500. El estado HTTP es un valor en la respuesta que ya tienes: compruebas response.ok, no capturas nada. Manejo de Errores con la Fetch API en JavaScript explica por qu茅 esto sorprende a quien espera que un error HTTP lance una excepci贸n, y c贸mo envolver fetch() para recuperar un comportamiento al estilo excepci贸n si lo quieres.
驴Cu谩ndo usar excepciones, y cu谩ndo devolver errores?
Hazte una pregunta sobre el fallo concreto: 驴quien llama de inmediato tiene que actuar sobre 茅l, o deber铆a viajar hacia arriba hasta que alg煤n l铆mite lo gestione?
Un cache miss o un error de validaci贸n en la entrada del usuario es un resultado que tu c贸digo deber铆a esperar y sobre el que ramificar ah铆 mismo. Un valor de error encaja. Quien llama no puede hacer su trabajo sin reaccionar, as铆 que un tipo de retorno que hace visible el fallo es una ayuda, no ceremonia.
Una conexi贸n a base de datos que se cae a mitad de una transacci贸n, o una aserci贸n sobre el estado del programa que resulta ser falsa, son distintas. Nada de lo que haga quien llama de inmediato lo va a arreglar. Dejar que se propague como excepci贸n o panic hasta un 煤nico lugar que lo registre y cierre limpiamente es menos c贸digo, y c贸digo m谩s honesto, que hacerlo atravesar manualmente cada frame.
Los lenguajes que adoptan un modelo por defecto ya usan el otro donde encaja. Go entra en panic ante un 铆ndice de slice fuera de rango, porque eso es un bug, no una condici贸n que quien llama deba manejar. Rust entra en panic ante un desbordamiento de enteros en builds de debug. El dict.get(key, default) de Python devuelve un valor en lugar de lanzar KeyError, para el caso en que una clave ausente se espera en vez de ser excepcional.
驴Qu茅 sale mal con cada estrategia en la pr谩ctica?
Errores tragados. Un except: vac铆o que no hace nada, o un if err != nil { } en Go con el cuerpo vac铆o, borra el fallo. El s铆ntoma aparece m谩s tarde como datos faltantes o una pantalla en blanco, sin ning煤n error cerca de la causa real. Es el flujo dif铆cil de rastrear descrito en Qu茅 Es el Spaghetti Code y C贸mo Refactorizarlo.
Panics en la goroutine equivocada. recover() solo funciona dentro de la goroutine que entr贸 en panic. Un recover diferido en main no te salva de un panic en una goroutine worker; todo el programa se cae. Goroutines y Channels en Go: Gu铆a con Ejemplos muestra por qu茅 los errores del trabajo concurrente deber铆an volver por un channel, como valores.
.unwrap() y un catch gen茅rico en lugar de una decisi贸n. Ambos te permiten dejar de pensar en el caso de fallo sin hacerlo desaparecer de verdad. Escribe una prueba que fuerce el camino de error. Es mucho m谩s f谩cil cuando la funci贸n devuelve un error que puedes construir t煤 mismo que cuando lo lanza desde dentro de una librer铆a que no controlas, y es la raz贸n por la que los caminos de error tienen pruebas propias en Pruebas Unitarias vs de Integraci贸n vs End-to-End.
驴C贸mo elegir entre excepci贸n y valor de error, funci贸n por funci贸n?
La pr贸xima vez que est茅s por escribir throw o return err, mira la funci贸n que tienes delante. Si quien la llama debe reaccionar de inmediato, pon el fallo en el tipo de retorno: (T, error) en Go, Result en Rust, None o un default en Python. Si no puede pasar nada 煤til hasta que el fallo llegue a un manejador de nivel superior, lanza una excepci贸n o un panic y man茅jalo una sola vez, en ese l铆mite. Luego a帽ade una prueba para el camino de error, cualquiera que hayas elegido.