Excepciones: "qué sucedió" vs "qué hacer"

19

Usamos excepciones para permitir que el consumidor del código maneje comportamientos inesperados de una manera útil. Por lo general, las excepciones se basan en el escenario "qué sucedió", como FileNotFound(no pudimos encontrar el archivo que especificó) o ZeroDivisionError(no pudimos realizar la 1/0operación).

¿Qué sucede si existe la posibilidad de especificar el comportamiento esperado del consumidor?

Por ejemplo, imagine que tenemos un fetchrecurso, que realiza la solicitud HTTP y devuelve los datos recuperados. Y en lugar de errores como ServiceTemporaryUnavailableo RateLimitExceededsimplemente plantearíamos una RetryableErrorsugerencia al consumidor de que debería volver a intentar la solicitud y no preocuparse por fallas específicas. Entonces, básicamente estamos sugiriendo una acción a la persona que llama: el "qué hacer".

No hacemos esto a menudo porque no conocemos todos los casos de uso de los consumidores. Pero imagine que es un componente específico que conocemos el mejor curso de acción para una persona que llama, ¿entonces deberíamos utilizar el enfoque de "qué hacer"?

Roman Bodnarchuk
fuente
3
¿HTTP ya no hace esto? 503 es una falla temporal de respuesta, por lo que el solicitante debe volver a intentarlo, 404 es una ausencia fundamental, por lo que no tiene sentido volver a intentarlo, 301 significa "movido permanentemente", por lo que debe volver a intentarlo, pero con una dirección diferente, etc.
Kilian Foth
77
En muchos casos, si realmente sabemos "qué hacer", podemos hacer que la computadora lo haga automáticamente y el usuario ni siquiera tenga que saber que algo salió mal. Supongo que cada vez que mi navegador recibe un 301 simplemente va a la nueva dirección sin preguntarme.
Ixrec
@ Ixrec - también tuve la misma idea. sin embargo, es posible que el consumidor no quiera esperar otra solicitud e ignore el artículo o falle por completo.
Roman Bodnarchuk
1
@RomanBodnarchuk: No estoy de acuerdo. Es como decir que una persona no debería necesitar saber chino para hablar chino. HTTP es un protocolo, y se espera que tanto el cliente como el servidor lo conozcan y lo sigan. Así es como funciona un protocolo. Si solo un lado lo sabe y lo cumple, entonces no puede comunicarse.
Chris Pratt
1
Honestamente, esto suena como si estuvieras tratando de reemplazar tus excepciones con un bloque catch. Es por eso que pasamos a las excepciones, no más if( ! function() ) handle_something();, pero al ser capaz de manejar el error en algún lugar que realmente conozca el contexto de la llamada, es decir, decirle a un cliente que llame a un administrador del sistema si su servidor falló o que se recargará automáticamente si se corta la conexión, pero le avisará caso la persona que llama es otro microservicio. Deje que los bloques de captura manejen la captura.
Sebb

Respuestas:

47

Pero imagine que es un componente específico que conocemos el mejor curso de acción para una persona que llama.

Esto casi siempre falla para al menos una de las personas que llaman, para lo cual este comportamiento es increíblemente irritante. No asumas que sabes mejor. Dígale a sus usuarios lo que está sucediendo, no lo que supone que deberían hacer al respecto. En muchos casos, ya está claro cuál debería ser un curso de acción sensato (y, si no lo es, haga una sugerencia en su manual de usuario).

Por ejemplo, incluso las excepciones dadas en su pregunta demuestran su suposición equivocada: a ServiceTemporaryUnavailableequivale a "intentarlo más tarde", y RateLimitExceededequivale a "woah allí relajarse, tal vez ajustar los parámetros del temporizador e intentar nuevamente en unos minutos". Pero el usuario también puede querer activar algún tipo de alarma ServiceTemporaryUnavailable(que indica un problema del servidor), y no para RateLimitExceeded(que no lo hace).

Dales la opción .

La ligereza corre con Mónica
fuente
44
Estoy de acuerdo. El servidor solo debe transmitir la información correctamente. La documentación, por otro lado, debe describir claramente el curso de acción adecuado en tales casos.
Neil
1
Una pequeña falla en esto es que para algunos piratas informáticos, ciertas excepciones pueden decirles mucho sobre lo que está haciendo su código y pueden usarlo para descubrir formas de explotarlo.
Pharap
3
@Pharap Si sus piratas informáticos tienen acceso a la excepción en lugar de un mensaje de error, ya está perdido.
corsiKa
2
Me gusta esta respuesta, pero me falta lo que considero un requisito de excepciones ... si supieras cómo recuperarte, ¡no sería una excepción! Una excepción solo debería ser cuando no puede hacer algo al respecto: entrada no válida, estado no válido, seguridad no válida: no puede solucionarlos mediante programación.
corsiKa
1
Convenido. Si insiste en indicar si un reintento es posible, siempre puede hacer que las excepciones concretas relevantes hereden de RetryableError.
sapi
18

¡Advertencia! ¡El programador de C ++ que viene aquí con ideas posiblemente diferentes de cómo se debe hacer el manejo de excepciones tratando de responder una pregunta que ciertamente es sobre otro lenguaje!

Ante esta idea:

Por ejemplo, imagine que tenemos un recurso de recuperación, que realiza una solicitud HTTP y devuelve los datos recuperados. Y en lugar de errores como ServiceTemporaryUnavailable o RateLimitExceeded, solo plantearíamos un RetryableError sugiriendo al consumidor que debería volver a intentar la solicitud y no preocuparse por fallas específicas.

... una cosa que sugeriría es que puede estar mezclando preocupaciones de informar un error con cursos de acción para responder de una manera que pueda degradar la generalidad de su código o requerir muchos "puntos de traducción" para las excepciones .

Por ejemplo, si modelo una transacción que implica cargar un archivo, podría fallar por varias razones. Quizás cargar el archivo implica cargar un complemento que no existe en la máquina del usuario. Quizás el archivo simplemente está dañado y encontramos un error al analizarlo.

Pase lo que pase, digamos que el curso de acción es informar lo que sucedió al usuario y preguntarle qué quiere hacer al respecto ("reintentar, cargar otro archivo, cancelar").

Lanzador contra receptor

Ese curso de acción se aplica independientemente del tipo de error que encontramos en este caso. No está integrado en la idea general de un error de análisis, no está integrado en la idea general de no cargar un complemento. Está incrustado en la idea de encontrar tales errores durante el contexto preciso de cargar un archivo (la combinación de cargar un archivo y fallar). Por lo general, lo veo, hablando en términos crudos, como la catcher'sresponsabilidad de determinar el curso de acción en respuesta a una excepción lanzada (por ejemplo, preguntar al usuario con opciones), no el thrower's.

Dicho de otra manera, los sitios cuyas throwexcepciones generalmente carecen de este tipo de información contextual, especialmente si las funciones que arrojan son generalmente aplicables. Incluso en un contexto totalmente desgeneralizado cuando tienen esta información, terminas arrinconándote en términos de comportamiento de recuperación al insertarlo en el throwsitio. Los sitios que catchgeneralmente tienen la mayor cantidad de información disponible para determinar un curso de acción y le brindan un lugar central para modificar si ese curso de acción alguna vez cambia para esa transacción dada.

Cuando comienzas a intentar lanzar excepciones que ya no informan qué está mal, sino que intentas determinar qué hacer, eso podría degradar la generalidad y flexibilidad de tu código. Un error de análisis no siempre debe conducir a este tipo de mensaje, varía según el contexto en el que se produce dicha excepción (la transacción en la que se produjo).

El lanzador ciego

Solo en general, gran parte del diseño del manejo de excepciones a menudo gira en torno a la idea de un lanzador ciego. No sabe cómo se detectará la excepción ni dónde. Lo mismo se aplica incluso a las formas más antiguas de recuperación de errores utilizando la propagación manual de errores. Los sitios que encuentran errores no incluyen un curso de acción del usuario, solo incrustan la información mínima para informar qué tipo de error se encontró.

Responsabilidades invertidas y generalización del receptor

Al pensar en esto con más cuidado, estaba tratando de imaginar el tipo de base de código donde esto podría convertirse en una tentación. Mi imaginación (posiblemente equivocada) es que su equipo todavía está desempeñando el papel de "consumidor" aquí e implementando la mayor parte del código de llamada también. Quizás tenga muchas transacciones dispares (muchos trybloques) que pueden encontrarse con los mismos conjuntos de errores, y todos deberían, desde una perspectiva de diseño, conducir a un curso uniforme de acción de recuperación.

Teniendo en cuenta los sabios consejos de una Lightness Races in Orbit'sbuena respuesta (que creo que realmente proviene de una mentalidad avanzada orientada a la biblioteca), aún podría sentirse tentado a lanzar excepciones de "qué hacer", solo más cerca del sitio de recuperación de transacciones.

Podría ser posible encontrar aquí un sitio intermediario y común de manejo de transacciones que realmente centralice las preocupaciones de "qué hacer", pero aún dentro del contexto de la captura.

ingrese la descripción de la imagen aquí

Esto solo se aplicaría si puede diseñar algún tipo de función general que utilicen todas estas transacciones externas (por ejemplo: una función que ingresa otra función para llamar o una clase base de transacción abstracta con comportamiento reemplazable que modela este sitio de transacciones intermedias que realiza la captura sofisticada )

Sin embargo, ese podría ser responsable de centralizar el curso de acción del usuario en respuesta a una variedad de posibles errores, y aún dentro del contexto de atrapar en lugar de tirar. Ejemplo simple (pseudocódigo de Python-ish, y no soy un desarrollador experimentado de Python en lo más mínimo, por lo que podría haber una forma más idiomática de hacerlo):

def general_catcher(task):
    try:
       task()
    except SomeError1:
       # do some uniformly-designed recovery stuff here
    except SomeError2:
       # do some other uniformly-designed recovery stuff here
    ...

[Ojalá con un nombre mejor que general_catcher]. En este ejemplo, puede pasar una función que contiene qué tarea realizar, pero aún así beneficiarse del comportamiento de captura generalizado / unificado para todos los tipos de excepciones que le interesan, y continuar extendiendo o modificando la parte "qué hacer" le gusta desde esta ubicación central y aún dentro de un catchcontexto en el que esto generalmente se fomenta. Lo mejor de todo es que podemos evitar que los sitios de lanzamiento se preocupen por "qué hacer" (preservando la noción de "lanzador ciego").

Si no encuentra ninguna de estas sugerencias útiles aquí y hay una fuerte tentación de lanzar excepciones de "qué hacer" de todos modos, tenga en cuenta que esto es muy anti-idiomático por lo menos, y también puede desalentar una mentalidad generalizada.

ChrisF
fuente
2
+1. Nunca antes había oído hablar de la idea del "Lanzador ciego", pero encaja en cómo pienso en el manejo de excepciones: declare que ocurrió el error, no piense en cómo se debe manejar. Cuando usted es responsable de la pila completa, es difícil (¡pero importante!) Separar las responsabilidades limpiamente, y la persona que llama es responsable de notificar sobre los problemas, y la persona que llama por manejar los problemas. El llamado solo sabe lo que se le pidió que hiciera, no por qué. Los errores de manejo deben hacerse en el contexto del 'por qué', por lo tanto: en la persona que llama.
Sjoerd Job Postmus
1
(Además: no creo que su respuesta sea específica de C ++, pero aplicable al manejo de excepciones en general)
Sjoerd Job Postmus
1
@SjoerdJobPostmus Yay gracias! El "Blind Thrower" es solo una analogía tonta que se me ocurrió aquí: no soy muy inteligente ni rápido para digerir conceptos técnicos complejos, por lo que a menudo quiero encontrar pequeñas imágenes y analogías para tratar de explicar y mejorar mi propia comprensión. de cosas. Tal vez algún día pueda tratar de escribir un pequeño libro de programación lleno de muchos dibujos animados. :-D
1
Je, esa es una pequeña imagen agradable. Un pequeño personaje de dibujos animados que lleva una venda en los ojos y arroja excepciones en forma de pelota de béisbol, no está seguro de quién los atrapará (o incluso si los atraparán), pero cumple con su deber como lanzador ciego.
Blacklight Shining
1
@DrunkCoder: Por favor, no destroces tus publicaciones. Ya tenemos suficientes cosas de borken en Internet. Si tiene una buena razón para la eliminación, marque su publicación para la atención del moderador y haga su caso.
Robert Harvey
2

Creo que la mayoría de las veces sería mejor pasar argumentos a la función diciéndole cómo manejar esas situaciones.

Por ejemplo, considere una función:

Response fetchUrl(URL url, RetryPolicy retryPolicy);

Puedo pasar RetryPolicy.noRetries () o RetryPolicy.retries (3) o lo que sea. En el caso de una falla recuperable, consultará la política para decidir si debe volver a intentarlo o no.

Winston Ewert
fuente
Sin embargo, eso no tiene mucho que ver con arrojar excepciones al sitio de la llamada. Estás hablando de algo un poco diferente, lo cual está bien pero no es realmente parte de la pregunta ..
Luminosidad razas con Monica
@LightnessRacesinOrbit, por el contrario. Lo presento como una alternativa a la idea de lanzar excepciones al sitio de la llamada. En el ejemplo del OP, fetchUrl arrojaría una RetryableException, y digo que, en cambio, debe decirle a fetchUrl cuándo debe volver a intentarlo.
Winston Ewert
2
@ WinstonEwert: Aunque estoy de acuerdo con LightnessRacesinOrbit, también entiendo tu punto, pero lo considero una forma diferente de representar el mismo control. Pero, tenga en cuenta que probablemente quiera pasar new RetryPolicy().onRateLimitExceeded(STOP).onServiceTemporaryUnavailable(RETRY, 3)o algo así, ya que es RateLimitExceededposible que deba manejarse de manera diferente ServiceTemporaryUnavailable. Después de escribir eso, mi pensamiento es: mejor lanzar una excepción, porque da un control más flexible.
Sjoerd Job Postmus
@SjoerdJobPostmus, creo que depende. Es muy posible que la lógica de limitación de velocidad y reintento se realice en su biblioteca, en cuyo caso creo que mi enfoque tiene sentido. Si tiene más sentido dejar eso a la persona que llama, tíralo por todos los medios.
Winston Ewert