exception - todas - ¿Por qué no usar excepciones como flujo regular de control?
try catch definicion (24)
Para evitar todas las respuestas estándar en las que podría haber buscado en Google, daré un ejemplo de que todos pueden atacar a voluntad.
C # y Java (y muchos otros) tienen muchos tipos de comportamiento de "desbordamiento" que no me gustan para nada (por ejemplo, type.MaxValue + type.SmallestValue == type.MinValue
por ejemplo: int.MaxValue + 1 == int.MinValue
).
Pero, visto mi naturaleza cruel, agregaré algún insulto a esta lesión al expandir este comportamiento a, digamos, un tipo de fecha y hora anulada. (Sé que DateTime
está sellado en .NET, pero por el bien de este ejemplo, estoy usando un pseudo lenguaje que es exactamente como C #, excepto por el hecho de que DateTime no está sellado).
El método de Add
anulado:
/// <summary>
/// Increments this date with a timespan, but loops when
/// the maximum value for datetime is exceeded.
/// </summary>
/// <param name="ts">The timespan to (try to) add</param>
/// <returns>The Date, incremented with the given timespan.
/// If DateTime.MaxValue is exceeded, the sum wil ''overflow'' and
/// continue from DateTime.MinValue.
/// </returns>
public DateTime override Add(TimeSpan ts)
{
try
{
return base.Add(ts);
}
catch (ArgumentOutOfRangeException nb)
{
// calculate how much the MaxValue is exceeded
// regular program flow
TimeSpan saldo = ts - (base.MaxValue - this);
return DateTime.MinValue.Add(saldo)
}
catch(Exception anyOther)
{
// ''real'' exception handling.
}
}
Por supuesto, un if podría resolver esto igual de fácil, pero el hecho es que no entiendo por qué no puedes usar excepciones (lógicamente, es decir, puedo ver que cuando el rendimiento es un problema, en ciertos casos deben evitarse excepciones). )
Creo que en muchos casos son más claras que las estructuras if y no rompen ningún contrato que esté haciendo el método.
En mi humilde opinión, la reacción "Nunca los use para el flujo regular del programa" que todo el mundo parece tener no es tan bueno como la fuerza de esa reacción puede justificar.
¿O estoy equivocado?
He leído otras publicaciones, tratando con todo tipo de casos especiales, pero mi punto es que no hay nada de malo si ambos:
- Claro
- Honre el contrato de su método
Dispararme.
¿Alguna vez ha intentado depurar un programa que genera cinco excepciones por segundo en el curso normal de la operación?
Yo tengo.
El programa era bastante complejo (era un servidor de cálculo distribuido), y una pequeña modificación en un lado del programa podía fácilmente romper algo en un lugar totalmente diferente.
Desearía haber lanzado el programa y esperar a que ocurrieran excepciones, pero hubo alrededor de 200 excepciones durante el inicio en el curso normal de las operaciones.
Mi punto: si usas excepciones para situaciones normales, ¿cómo localizas situaciones inusuales (es decir, una excepción )?
Por supuesto, hay otras razones poderosas para no usar demasiadas excepciones, especialmente en lo que respecta al rendimiento
¿Qué tal el rendimiento? Mientras realizamos una prueba de carga en una aplicación web .NET, superamos los 100 usuarios simulados por servidor web hasta que solucionamos una excepción común y ese número aumentó a 500 usuarios.
Además de los motivos indicados, una razón para no usar excepciones para el control de flujo es que puede complicar en gran medida el proceso de depuración.
Por ejemplo, cuando intento rastrear un error en VS, normalmente enciendo "romper todas las excepciones". Si usa excepciones para el control de flujo, voy a interrumpir regularmente el depurador y tendré que seguir ignorando estas excepciones no excepcionales hasta que llegue el problema real. ¡Esto probablemente volverá loco a alguien!
Antes de las excepciones, en C, había setjmp
y longjmp
que se podían usar para lograr un desenrollamiento similar del marco de pila.
Luego, al mismo constructo se le dio un nombre: "Excepción". Y la mayoría de las respuestas se basan en el significado de este nombre para discutir sobre su uso. Está destinado a ser utilizado para condiciones excepcionales. Eso nunca se reflejó en el longjmp
original. Hubo situaciones en las que necesitó romper el flujo de control en muchos marcos de pila.
Las excepciones son un poco más generales ya que también puedes usarlas dentro del mismo marco de pila. Esto plantea analogías con goto
que creo que están equivocadas. Los Gotos son un par estrechamente acoplado (y también lo son setjmp
y longjmp
). Las excepciones siguen a una publicación / suscripción poco unida que es mucho más clara.
La tercera fuente de confusión se relaciona con si se marcan o no las excepciones. Por supuesto, las excepciones no revisadas parecen particularmente horribles de usar para el flujo de control.
Las excepciones controladas sin embargo son geniales para el flujo de control, una vez que superas todas las obsesiones victorianas y vives un poco.
Mi uso favorito es una secuencia de throw new Success()
en un fragmento largo de código que intenta una cosa tras otra hasta que encuentra lo que está buscando. Cada cosa, cada parte de la lógica, puede tener un anidamiento arbritrario, por lo que las break
están fuera, como también cualquier tipo de prueba de condición. El patrón if-else
es frágil. Si edito un else
o estropeo la sintaxis de alguna otra manera, entonces hay un error peludo.
El uso de throw new Success()
linealiza el flujo de código. Utilizo clases de Success
definidas localmente, comprobadas por supuesto, de modo que si me olvido de atraparlo, el código no se compilará. Y no entiendo el Success
otro método.
A veces mi código busca una cosa después de la otra y solo tiene éxito si todo está bien. En este caso, tengo una linealización similar utilizando throw new Failure()
.
Usar una función separada complica el nivel natural de compartimentación. Entonces la solución de return
no es óptima. Prefiero tener una página o dos de código en un solo lugar por razones cognitivas. No creo en el código ultra-finamente dividido.
Lo que las JVM o compiladores hacen es menos relevante para mí a menos que haya un punto de acceso. No puedo creer que exista una razón fundamental para que los compiladores no detecten Excepciones lanzadas y capturadas localmente y simplemente las traten como eficientes a nivel de código máquina.
En cuanto a utilizarlos en funciones para el flujo de control, es decir, para casos comunes en lugar de excepcionales, no veo cómo serían menos eficientes que las pruebas de condición de corte múltiple, regresa a vadear a través de tres marcos de pila en lugar de solo restaurar el puntero de pila
Personalmente, no utilizo el patrón en los marcos de pila y puedo ver cómo requeriría sofisticación de diseño para hacerlo elegantemente. Pero usado con moderación, debería estar bien.
Por último, con respecto a los programadores virginales sorprendentes, no es una razón convincente. Si los presentas suavemente a la práctica, aprenderán a amarla. Recuerdo que C ++ solía sorprender y asustar a los programadores de C.
Creo que puedes usar Excepciones para control de flujo. Sin embargo, hay una otra parte de esta técnica. Crear excepciones es una tarea costosa, porque tienen que crear un seguimiento de pila. Por lo tanto, si desea usar Excepciones con más frecuencia que solo para señalar una situación excepcional, debe asegurarse de que la construcción de los rastros de pila no influya negativamente en su rendimiento.
La mejor manera de reducir el costo de crear excepciones es anular el método fillInStackTrace () de esta manera:
public Throwable fillInStackTrace() { return this; }
Tal excepción no tendrá rellenos de pila.
Debido a que el código es difícil de leer, puede tener problemas para depurarlo, introducirá nuevos errores al corregir errores después de mucho tiempo, es más costoso en términos de recursos y tiempo, y le molesta si está depurando su código y el depurador se detiene ante la ocurrencia de cada excepción;)
El estándar es que las excepciones no son regulares y deben usarse en casos excepcionales.
Una razón, que es importante para mí, es que cuando leo una estructura de control try-catch
en un software que mantengo o depuro, trato de averiguar por qué el codificador original utilizó un manejo de excepción en lugar de una estructura if-else
. Y espero encontrar una buena respuesta.
Recuerde que escribe código no solo para la computadora sino también para otros codificadores. Hay una semántica asociada a un manejador de excepciones que no se puede descartar simplemente porque a la máquina no le importe.
Josh Bloch se ocupa extensamente de este tema en Java efectivo. Sus sugerencias son esclarecedoras y deberían aplicarse a .Net también (a excepción de los detalles).
En particular, las excepciones deben usarse para circunstancias excepcionales. Las razones para esto están relacionadas con la usabilidad, principalmente. Para que un método dado sea utilizable al máximo, sus condiciones de entrada y salida deben estar limitadas al máximo.
Por ejemplo, el segundo método es más fácil de usar que el primero:
/**
* Adds two positive numbers.
*
* @param addend1 greater than zero
* @param addend2 greater than zero
* @throws AdditionException if addend1 or addend2 is less than or equal to zero
*/
int addPositiveNumbers(int addend1, int addend2) throws AdditionException{
if( addend1 <= 0 ){
throw new AdditionException("addend1 is <= 0");
}
else if( addend2 <= 0 ){
throw new AdditionException("addend2 is <= 0");
}
return addend1 + addend2;
}
/**
* Adds two positive numbers.
*
* @param addend1 greater than zero
* @param addend2 greater than zero
*/
public int addPositiveNumbers(int addend1, int addend2) {
if( addend1 <= 0 ){
throw new IllegalArgumentException("addend1 is <= 0");
}
else if( addend2 <= 0 ){
throw new IllegalArgumentException("addend2 is <= 0");
}
return addend1 + addend2;
}
En cualquier caso, debe verificar que la persona que llama esté usando su API de manera apropiada. Pero en el segundo caso, lo requiere (implícitamente). Las excepciones suaves aún se lanzarán si el usuario no leyó el javadoc, pero:
- No necesita documentarlo.
- No es necesario que lo pruebes (dependiendo de cuán agresiva sea la estrategia de prueba de tu unidad).
- No requiere que la persona que llama maneje tres casos de uso.
El punto a nivel del suelo es que las Excepciones no deben usarse como códigos de retorno, en gran parte porque has complicado no solo tu API, sino también la API de la persona que llama.
Hacer lo correcto tiene un costo, por supuesto. El costo es que todos necesitan entender que necesitan leer y seguir la documentación. Espero que ese sea el caso de todos modos.
Las excepciones son básicamente declaraciones goto
no locales con todas las consecuencias de este último. El uso de excepciones para el control de flujo viola un principio de menor asombro , hace que los programas sean difíciles de leer (recuerde que los programas se escriben primero para los programadores).
Además, esto no es lo que esperan los proveedores de compiladores. Esperan que las excepciones sean lanzadas raramente, y generalmente permiten que el código de throw
sea bastante ineficiente. Lanzar excepciones es una de las operaciones más caras en .NET.
Sin embargo, algunos lenguajes (notablemente Python) usan excepciones como construcciones de control de flujo. Por ejemplo, los iteradores StopIteration
una excepción StopIteration
si no hay más elementos. Incluso las construcciones de lenguaje estándar (como for
ejemplo) se basan en esto.
Mi regla de oro es:
- Si puede hacer algo para recuperarse de un error, capture excepciones
- Si el error es muy común (por ejemplo, el usuario intentó iniciar sesión con la contraseña incorrecta), use los valores de retorno
- Si no puedes hacer nada para recuperarte de un error, déjalo sin capturar (O atrapalo en tu receptor principal para hacer un apagado semi-elegante de la aplicación)
El problema que veo con excepciones es desde un punto de vista puramente sintáctico (estoy bastante seguro de que la sobrecarga de rendimiento es mínima). No me gustan los try-blocks por todos lados.
Toma este ejemplo:
try
{
DoSomeMethod(); //Can throw Exception1
DoSomeOtherMethod(); //Can throw Exception1 and Exception2
}
catch(Exception1)
{
//Okay something messed up, but is it SomeMethod or SomeOtherMethod?
}
.. Otro ejemplo podría ser cuando necesitas asignar algo a un manipulador usando una fábrica, y esa fábrica podría arrojar una excepción:
Class1 myInstance;
try
{
myInstance = Class1Factory.Build();
}
catch(SomeException)
{
// Couldn''t instantiate class, do something else..
}
myInstance.BestMethodEver(); // Will throw a compile-time error, saying that myInstance is uninitalized, which it potentially is.. :(
Soo, personalmente, creo que deberías mantener excepciones para condiciones de error raras (memoria insuficiente, etc.) y usar valores de retorno (clases de valor, estructuras o enumeraciones) para hacer tu comprobación de errores en su lugar.
Espero haber entendido tu pregunta correcta :)
Por lo general, no hay nada de malo en manejar una excepción en un nivel bajo. Una excepción ES un mensaje válido que brinda muchos detalles sobre por qué no se puede realizar una operación. Y si puedes manejarlo, deberías.
En general, si sabes que hay una alta probabilidad de falla que puedes verificar ... debes hacer el cheque ... es decir, si (obj! = Null) obj.method ()
En su caso, no estoy lo suficientemente familiarizado con la biblioteca de C # para saber si la fecha y hora tiene una manera fácil de verificar si una marca de tiempo está fuera de los límites. Si lo hace, simplemente llame si (.isvalid (ts)) de lo contrario su código es básicamente correcto.
Entonces, básicamente todo se reduce a crear un código más limpio ... si la operación para protegerse contra una excepción esperada es más compleja que solo manejar la excepción; de lo que tiene mi permiso para manejar la excepción en lugar de crear guardias complejos en todas partes.
Puede usar una garra de martillo para girar un tornillo, al igual que puede usar excepciones para el flujo de control. Eso no significa que sea el uso previsto de la función. La sentencia if
expresa las condiciones, cuyo uso previsto es controlar el flujo.
Si está utilizando una función de forma involuntaria al elegir no usar la función diseñada para tal fin, habrá un costo asociado. En este caso, la claridad y el rendimiento no tienen ningún valor añadido real. ¿Qué significa que el uso de excepciones te compre sobre la declaración if
ampliamente aceptada?
Dicho de otra manera: solo porque puedes no significa que debas .
Realmente no veo cómo estás controlando el flujo del programa en el código que citaste. Nunca verá otra excepción además de la excepción ArgumentOutOfRange. (Entonces su segunda cláusula catch nunca será golpeada). Todo lo que estás haciendo es usar un tiro extremadamente costoso para imitar una declaración if.
Además, no está ejecutando las operaciones más siniestras en las que solo lanza una excepción solo para que sea atrapada en otro lugar para realizar un control de flujo. En realidad estás manejando un caso excepcional.
Si está utilizando manejadores de excepciones para el flujo de control, está siendo demasiado general y vago. Como alguien más mencionó, usted sabe que algo sucedió si está manejando el procesamiento en el controlador, pero ¿qué es exactamente? Básicamente está utilizando la excepción para una instrucción else, si la está usando para flujo de control.
Si no sabe qué estado podría ocurrir, puede usar un manejador de excepciones para estados inesperados, por ejemplo, cuando tiene que usar una biblioteca de terceros, o tiene que atrapar todo en la interfaz de usuario para mostrar un buen error mensaje y registrar la excepción.
Sin embargo, si sabe qué podría salir mal, y no pone una declaración if o algo para verificar, entonces simplemente está siendo flojo. Permitir que el manejador de excepciones sea la solución para todo lo que sabe que podría pasar es perezoso, y volverá para atormentarlo más tarde, porque tratará de arreglar una situación en su manejador de excepciones basándose en una suposición posiblemente falsa.
Si pones la lógica en tu manejador de excepciones para determinar qué pasó exactamente, entonces serías bastante estúpido por no poner esa lógica dentro del bloque try.
Los manejadores de excepciones son el último recurso, cuando se quedan sin ideas / formas de evitar que algo vaya mal, o las cosas están más allá de su capacidad de control. Like, the server is down and times out and you can''t prevent that exception from being thrown.
Finally, having all the checks done up front shows what you know or expect will occur and makes it explicit. Code should be clear in intent. What would you rather read?
Supongamos que tienes un método que hace algunos cálculos. Hay muchos parámetros de entrada que debe validar y luego devolver un número mayor que 0.
El uso de valores de retorno para señalar un error de validación, es simple: si el método devolvió un número menor que 0, se produjo un error. ¿Cómo saber qué parámetro no se validó?
Recuerdo que desde mis días C muchas funciones devolvían códigos de error como este:
-1 - x lesser then MinX
-2 - x greater then MaxX
-3 - y lesser then MinY
etc.
¿Es realmente menos legible que lanzar y atrapar una excepción?
Una primera reacción a muchas respuestas:
estás escribiendo para los programadores y el principio de menos asombro
¡Por supuesto! Pero si simplemente no está más claro todo el tiempo.
No debería ser sorprendente, por ejemplo: dividir (1 / x) captura (divisiónByZero) es más claro que cualquier otro para mí (en Conrad y otros). El hecho de que este tipo de programación no se espera es puramente convencional, y de hecho, sigue siendo relevante. Quizás en mi ejemplo un if sería más claro.
Pero DivisionByZero y FileNotFound para ese asunto son más claros que ifs.
Por supuesto, si es menos eficiente y necesita un trillón de veces por segundo, debe evitarlo, pero aún no he leído ninguna buena razón para evitar el diseño general.
En lo que respecta al principio de menor asombro: aquí existe el peligro de un razonamiento circular: supongamos que toda una comunidad utiliza un diseño malo, ¡se espera que este diseño! Por lo tanto, el principio no puede ser un grial y debe ser considerado cuidadosamente.
excepciones para situaciones normales, ¿cómo localizas situaciones inusuales (es decir, excepcionales)?
En muchas reacciones sth. así brilla este canal. Solo atraparlos, ¿no? Su método debe ser claro, estar bien documentado y contar con su contrato. No entiendo esa pregunta, debo admitirlo.
Depuración en todas las excepciones: lo mismo, eso se hace algunas veces porque el diseño de no usar excepciones es común. Mi pregunta era: ¿por qué es común en primer lugar?
As others have mentioned numerously, the principle of least astonishment will forbid that you use exceptions excessively for control flow only purposes. On the other hand, no rule is 100% correct, and there are always those cases where an exception is "just the right tool" - much like goto
itself, by the way, which ships in the form of break
and continue
in languages like Java, which are often the perfect way to jump out of heavily nested loops, which aren''t always avoidable.
The following blog post explains a rather complex but also rather interesting use-case for a non-local ControlFlowException
:
It explains how inside of jOOQ (a SQL abstraction library for Java) , such exceptions are occasionally used to abort the SQL rendering process early when some "rare" condition is met.
Examples of such conditions are:
Too many bind values are encountered. Some databases do not support arbitrary numbers of bind values in their SQL statements (SQLite: 999, Ingres 10.1.0: 1024, Sybase ASE 15.5: 2000, SQL Server 2008: 2100). In those cases, jOOQ aborts the SQL rendering phase and re-renders the SQL statement with inlined bind values. Ejemplo:
// Pseudo-code attaching a "handler" that will // abort query rendering once the maximum number // of bind values was exceeded: context.attachBindValueCounter(); String sql; try { // In most cases, this will succeed: sql = query.render(); } catch (ReRenderWithInlinedVariables e) { sql = query.renderWithInlinedBindValues(); }
If we explicitly extracted the bind values from the query AST to count them every time, we''d waste valuable CPU cycles for those 99.9% of the queries that don''t suffer from this problem.
Some logic is available only indirectly via an API that we want to execute only "partially". The
UpdatableRecord.store()
method generates anINSERT
orUPDATE
statement, depending on theRecord
''s internal flags. From the "outside", we don''t know what kind of logic is contained instore()
(eg optimistic locking, event listener handling, etc.) so we don''t want to repeat that logic when we store several records in a batch statement, where we''d like to havestore()
only generate the SQL statement, not actually execute it. Ejemplo:// Pseudo-code attaching a "handler" that will // prevent query execution and throw exceptions // instead: context.attachQueryCollector(); // Collect the SQL for every store operation for (int i = 0; i < records.length; i++) { try { records[i].store(); } // The attached handler will result in this // exception being thrown rather than actually // storing records to the database catch (QueryCollectorException e) { // The exception is thrown after the rendered // SQL statement is available queries.add(e.query()); } }
If we had externalised the
store()
logic into "re-usable" API that can be customised to optionally not execute the SQL, we''d be looking into creating a rather hard to maintain, hardly re-usable API.
Conclusión
In essence, our usage of these non-local goto
s is just along the lines of what [Mason Wheeler][5] said in his answer:
"I just encountered a situation that I cannot deal with properly at this point, because I don''t have enough context to handle it, but the routine that called me (or something further up the call stack) ought to know how to handle it."
Both usages of ControlFlowExceptions
were rather easy to implement compared to their alternatives, allowing us to reuse a wide range of logic without refactoring it out of the relevant internals.
But the feeling of this being a bit of a surprise to future maintainers remains. The code feels rather delicate and while it was the right choice in this case, we''d always prefer not to use exceptions for local control flow, where it is easy to avoid using ordinary branching through if - else
.
But you won''t always know what happens in the Method/s that you call. You won''t know exactly where the exception was thrown. Without examining the exception object in greater detail....
Here are best practices I described in my blog post :
- Throw an exception to state an unexpected situation in your software.
- Use return values for input validation .
- If you know how to deal with exceptions a library throws, catch them at the lowest level possible .
- If you have an unexpected exception, discard current operation completely. Don''t pretend you know how to deal with them .
I don''t think there is anything wrong with using Exceptions for flow-control. Exceptions are somewhat similar to continuations and in statically typed languages, Exceptions are more powerful than continuations, so, if you need continuations but your language doesn''t have them, you can use Exceptions to implement them.
Well, actually, if you need continuations and your language doesn''t have them, you chose the wrong language and you should rather be using a different one. But sometimes you don''t have a choice: client-side web programming is the prime example – there''s just no way to get around JavaScript.
An example: Microsoft Volta is a project to allow writing web applications in straight-forward .NET, and let the framework take care of figuring out which bits need to run where. One consequence of this is that Volta needs to be able to compile CIL to JavaScript, so that you can run code on the client. However, there is a problem: .NET has multithreading, JavaScript doesn''t. So, Volta implements continuations in JavaScript using JavaScript Exceptions, then implements .NET Threads using those continuations. That way, Volta applications that use threads can be compiled to run in an unmodified browser – no Silverlight needed.
I feel that there is nothing wrong with your example. On the contrary, it would be a sin to ignore the exception thrown by the called function.
In the JVM, throwing an exception is not that expensive, only creating the exception with new xyzException(...), because the latter involves a stack walk. So if you have some exceptions created in advance, you may throw them many times without costs. Of course, this way you can''t pass data along with the exception, but I think that is a bad thing to do anyway.
One aesthetic reason:
A try always comes with a catch, whereas an if doesn''t have to come with an else.
if (PerformCheckSucceeded())
DoSomething();
With try/catch, it becomes much more verbose.
try
{
PerformCheckSucceeded();
DoSomething();
}
catch
{
}
That''s 6 lines of code too many.
There are a few general mechanisms via which a language could allow for a method to exit without returning a value and unwind to the next "catch" block:
Have the method examine the stack frame to determine the call site, and use the metadata for the call site to find either information about a
try
block within the calling method, or the location where the calling method stored the address of its caller; in the latter situation, examine metadata for the caller''s caller to determine in the same fashion as the immediate caller, repeating until one finds atry
block or the stack is empty. This approach adds very little overhead to the no-exception case (it does preclude some optimizations) but is expensive when an exception occurs.Have the method return a "hidden" flag which distinguishes a normal return from an exception, and have the caller check that flag and branch to an "exception" routine if it''s set. This routine adds 1-2 instructions to the no-exception case, but relatively little overhead when an exception occurs.
Have the caller place exception-handling information or code at a fixed address relative to the stacked return address. For example, with the ARM, instead of using the instruction "BL subroutine", one could use the sequence:
adr lr,next_instr b subroutine b handle_exception next_instr:
To exit normally, the subroutine would simply do bx lr
or pop {pc}
; in case of an abnormal exit, the subroutine would either subtract 4 from LR before performing the return or use sub lr,#4,pc
(depending upon the ARM variation, execution mode, etc.) This approach will malfunction very badly if the caller is not designed to accommodate it.
A language or framework which uses checked exceptions might benefit from having those handled with a mechanism like #2 or #3 above, while unchecked exceptions are handled using #1. Although the implementation of checked exceptions in Java is rather nuisancesome, they would not be a bad concept if there were a means by which a call site could say, essentially, "This method is declared as throwing XX, but I don''t expect it ever to do so; if it does, rethrow as an "unchecked" exception. In a framework where checked exceptions were handled in such fashion, they could be an effective means of flow control for things like parsing methods which in some contexts may have a high likelihood of failure, but where failure should return fundamentally different information than success. I''m unaware of any frameworks that use such a pattern, however. Instead, the more common pattern is to use the first approach above (minimal cost for the no-exception case, but high cost when exceptions are thrown) for all exceptions.
You might be interested in having a look at Common Lisp''s condition system which is a sort of generalization of exceptions done right. Because you can unwind the stack or not in a controlled way, you get "restarts" as well, which are extremely handy.
This doesn''t have anything much to do with best practices in other languages, but it shows you what can be done with some design thought in (roughly) the direction you are thinking of.
Of course there are still performance considerations if you''re bouncing up and down the stack like a yo-yo, but it''s a much more general idea than "oh crap, lets bail" kind of approach that most catch/throw exception systems embody.