Cómo manejar los errores correctamente: el silencio no siempre es bueno



Nunca he tenido ninguna opinión particular sobre el manejo de errores. Si comencé a trabajar con el código existente, continué realizando la tarea en la que trabajó el autor de la fuente; Si escribí el código desde cero, hice lo que me pareció correcto.

Pero recientemente, me encontré con un problema, un error que se manifestó debido a un error "silencioso" en el código. Me di cuenta de que hay algo en lo que reflexionar. Tal vez no pueda cambiar la forma en que manejo los errores en toda la base de código en la que estoy trabajando, pero algo definitivamente se puede optimizar.

Le recordamos: para todos los lectores de "Habr": un descuento de 10.000 rublos al registrarse en cualquier curso de Skillbox con el código de promoción "Habr".

Skillbox recomienda: El curso educativo en línea "Profession Java-developer" .


No siempre vale la pena cortar el hombro


El primer paso en el manejo de errores debe ser comprender cuando "error" no es "error". Por supuesto, todo esto depende de la lógica empresarial de su aplicación, pero en general, algunos errores son explícitos y se pueden solucionar sin problemas.

  • ¬ŅTiene un rango de fechas donde el "antes" es anterior al "de"? Reordenar
  • ¬ŅTiene un n√ļmero de tel√©fono que comienza con + o contiene un gui√≥n donde no espera que aparezcan caracteres especiales? Eliminarlos.
  • ¬ŅProblema de recolecci√≥n nula? Aseg√ļrese de inicializar esto antes del acceso (usando la inicializaci√≥n diferida o el constructor).

No interrumpa la ejecución del código debido a errores que puede corregir y, por supuesto, no interfiera con sus acciones a los usuarios de su servicio o aplicación. Si puede comprender el problema y resolverlo sobre la marcha, simplemente hágalo.



Devolver nulos u otros n√ļmeros m√°gicos


Valores cero, ‚Äď1 donde se espera un n√ļmero positivo y otros valores m√°gicos de retorno: todo esto es producto del demonio, que transfiere la responsabilidad de verificar los errores a la funci√≥n de llamada.

Con ellos, su código estará lleno de bloques que harán que la lógica de la aplicación no sea clara:

return_value = possibly_return_a_magic_value() if return_value < 0: handle_error() else: do_something() other_return_value = possibly_nullable_value() if other_return_value is None: handle_null_value() else: do_some_other_thing() 

Bueno, o más simple, pero también malo:
 var item = returnSomethingWhichCouldBeNull(); var result = item?.Property?.MaybeExists; if (result.HasValue) { DoSomething(); } 

Pasar nulo a los m√©todos tambi√©n es un problema, aunque en el c√≥digo de algunos desarrolladores puede encontrar lugares donde los m√©todos comienzan con varias l√≠neas de validaci√≥n de entrada. Pero, de hecho, todo esto no es necesario. La mayor√≠a de los lenguajes modernos proporcionan no una, sino varias herramientas a la vez, que le permiten indicar claramente lo que espera. Tambi√©n omiten las comprobaciones in√ļtiles, por ejemplo, definiendo par√°metros como distintos de cero o con el decorador correspondiente.

Códigos de error


Este es el mismo problema que con valores nulos y otros valores similares cuando se agrega una complicación de código innecesaria.

Por ejemplo, puede usar esta construcción para devolver un código de error:

 int errorCode; var result = getSomething(out errorCode); if (errorCode != 0) { doSomethingWithResult(result); } 

El resultado se puede mostrar de la siguiente manera:
 public class Result<T> { public T Item { get; set; } // At least "ErrorCode" is an enum public ErrorCode ErrorCode { get; set; } = ErrorCode.None; public IsError { get { return ErrorCode != ErrorCode.None; } } } public class UsingResultConstruct { ... var result = GetResult(); if (result.IsError) { switch (result.ErrorCode) { case ErrorCode.NetworkError: HandleNetworkError(); break; case ErrorCode.UserError: HandleUserError(); break; default: HandleUnknownError(); break; } } ActuallyDoSomethingWithResult(result); ... } 

Este no es realmente el mejor c√≥digo. Resulta que el art√≠culo a√ļn puede estar vac√≠o. De hecho, no hay garant√≠a (aparte de un acuerdo) de que cuando el resultado no contenga un error, pueda acceder de forma segura al Art√≠culo.

Despu√©s de terminar con todo este procesamiento, a√ļn tiene que convertir el c√≥digo de error en un mensaje de error y hacer algo con √©l. A veces sucede que, debido a la excesiva complejidad del c√≥digo, este mensaje en s√≠ no se muestra como deber√≠a.

Existe un problema a√ļn m√°s grave: si usted u otra persona cambia la implementaci√≥n interna para manejar un nuevo estado no v√°lido con un nuevo c√≥digo de error, entonces toda la estructura dejar√° de funcionar.

Si no funcionó la primera vez, intente nuevamente


Antes de continuar, vale la pena enfatizar que una falla "silenciosa" del programa es mala. Un problema inesperado puede ocurrir en el momento m√°s inoportuno, por ejemplo, el fin de semana, cuando no puede solucionar todo r√°pidamente.



Si lees C√≥digo limpio , entonces probablemente te est√©s preguntando ¬Ņpor qu√© no simplemente lanzar una excepci√≥n? Si no, entonces lo m√°s probable es que pienses que las excepciones son la ra√≠z del mal. Yo tambi√©n pensaba eso, pero ahora pienso un poco diferente.
Un punto interesante, al menos para mí, es que la implementación predeterminada para el nuevo método en C # es lanzar una excepción NotImplementedException, mientras que el valor predeterminado para el nuevo método en Python es "pasar".

Como resultado, a menudo ingresamos la configuración de "error silencioso" para Python. Me pregunto cuántos desarrolladores pasaron mucho tiempo para comprender lo que está sucediendo y por qué el programa no funciona. Pero al final, después de muchas horas, descubrieron que olvidaron implementar el método de marcador de posición.
Pero mira esto:

 public MyDataObject UpdateSomething(MyDataObject toUpdate) { if (_dbConnection == null) { throw new DbConnectionError(); } try { var newVersion = _dbConnection.Update(toUpdate); if (newVersion == null) { return null; } MyDataObject result = new MyDataObject(newVersion); return result; } catch (DbConnectionClosedException dbcc) { throw new DbConnectionError(); } catch (MyDataObjectUnhappyException dou) { throw new MalformedDataException(); } catch (Exception ex) { throw new UnknownErrorException(); } } 

Por supuesto, solo las excepciones no lo proteger√°n de la creaci√≥n de c√≥digo ilegible y no administrado. Debe aplicar excepciones como una estrategia bien pensada y bien equilibrada. De lo contrario, si el proyecto es demasiado grande, su aplicaci√≥n puede estar en un estado inconsistente. Por el contrario, si el proyecto es demasiado peque√Īo, obtendr√° el caos.

Para evitar que esto suceda, le aconsejo que tenga esto en cuenta:

Consistencia sobre todo. Siempre debe asegurarse de que la aplicación sea coherente. Si esto significa que tiene que ajustar cada par de líneas con un bloque try / catch, simplemente oculte todo en otra función.

 def my_function(): try: do_this() do_that() except: something_bad_happened() finally: cleanup_resource() 

La consolidaci√≥n de errores es necesaria. Es bueno si proporciona diferentes tipos de manejo de diferentes tipos de errores. Sin embargo, esto es para usted, no para los usuarios. Para ellos, lanza la √ļnica excepci√≥n para que los usuarios simplemente sepan que algo sali√≥ mal. Los detalles son su √°rea de responsabilidad.

 public MyDataObject UpdateSomething(MyDataObject toUpdate) { try { var newVersion = _dbConnection.Update(toUpdate); MyDataObject result = new MyDataObject(newVersion); return result; } catch (DbConnectionClosedException dbcc) { HandleDbConnectionClosed(); throw new UpdateMyDataObjectException(); } catch (MyDataObjectUnhappyException dou) { RollbackVersion(); throw new UpdateMyDataObjectException(); } catch (Exception ex) { throw new UpdateMyDataObjectException(); } } 

Vale la pena atrapar excepciones de inmediato. Se interceptan mejor en el nivel más bajo. Mantenerse constante y ocultar los detalles vale la pena como se describe anteriormente. El manejo de errores debe dejarse en el nivel superior de la aplicación. Si todo se hace correctamente, puede separar el flujo lógico de la aplicación del flujo de procesamiento de errores. Esto le permitirá escribir código claro y legible.

 def my_api(): try: item = get_something_from_the_db() new_version = do_something_to_item(item) return new_version except Exception as ex: handle_high_level_exception(ex) 

Eso es todo por ahora, y si desea analizar el tema de los errores y su procesamiento, bienvenido.

Skillbox recomienda:

Source: https://habr.com/ru/post/441148/


All Articles