Comment gérer correctement les erreurs: le silence n'est pas toujours bon



Je n'ai jamais eu d'opinion particulière concernant la gestion des erreurs. Si j'ai commencé à travailler avec du code existant, j'ai continué à effectuer la tâche sur laquelle l'auteur de la source a travaillé; si j'ai écrit le code à partir de zéro, j'ai fait ce qui me semblait le mieux.

Mais récemment, j'ai rencontré un problème, un bogue qui s'est manifesté en raison d'une erreur «silencieuse» dans le code. J'ai réalisé qu'il y avait quelque chose à réfléchir. Peut-être que je ne peux pas changer la façon dont je gère les erreurs dans toute la base de code sur laquelle je travaille, mais quelque chose peut certainement être optimisé.

Nous vous rappelons: pour tous les lecteurs de «Habr» - une remise de 10 000 roubles lors de l'inscription à un cours Skillbox en utilisant le code promo «Habr».

Skillbox recommande: Le cours éducatif en ligne "Profession Java-developer" .


Ça ne vaut pas toujours la peine de couper l'épaule


La première étape de la gestion des erreurs doit être de comprendre quand «erreur» n'est pas «erreur!» Bien sûr, tout cela dépend de la logique métier de votre application, mais en général, certains bugs sont explicites et peuvent être corrigés sans problème.

  • Avez-vous une plage de dates où le «avant» est avant le «de»? Réorganiser.
  • Avez-vous un numéro de téléphone commençant par + ou contenant un tiret où vous ne vous attendez pas à ce que des caractères spéciaux apparaissent? Retirez-les.
  • Problème de collecte nul? Assurez-vous de l'initialiser avant l'accès (en utilisant l' initialisation paresseuse ou le constructeur).

N'interrompez pas l'exécution de code en raison d'erreurs que vous pouvez corriger et, bien sûr, n'interférez pas avec vos actions auprès des utilisateurs de votre service ou application. Si vous êtes en mesure de comprendre le problème et de le résoudre à la volée, faites-le.



Renvoyer Null ou d'autres nombres magiques


Valeurs nulles, -1 où un nombre positif est attendu et autres valeurs de retour magiques - tout cela est le produit du diable, qui transfère la responsabilité de la vérification des erreurs à la fonction appelante.

Avec eux, votre code sera plein de tels blocs qui rendront la logique d'application peu claire:

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() 

Eh bien, ou plus simple, mais aussi mauvais:
 var item = returnSomethingWhichCouldBeNull(); var result = item?.Property?.MaybeExists; if (result.HasValue) { DoSomething(); } 

Passer le null aux méthodes est également un problème, bien que dans le code de certains développeurs, vous puissiez trouver des endroits où les méthodes commencent par plusieurs lignes de validation d'entrée. Mais en fait, tout cela n'est pas nécessaire. La plupart des langues modernes ne fournissent pas un, mais plusieurs outils à la fois, ce qui vous permet d'indiquer clairement ce que vous attendez. Ils sautent également les vérifications inutiles, par exemple, en définissant des paramètres non différents de zéro ou avec le décorateur correspondant.

Codes d'erreur


C'est le même problème qu'avec null et d'autres valeurs similaires lorsque des complications de code inutiles sont ajoutées.

Par exemple, vous pouvez utiliser cette construction pour renvoyer un code d'erreur:

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

Le résultat peut être affiché comme suit:
 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); ... } 

Ce n'est vraiment pas le meilleur code. Il s'avère que l'article peut toujours être vide. En fait, il n'y a aucune garantie (autre qu'un accord) que lorsque le résultat ne contient pas d'erreur, vous pouvez accéder en toute sécurité à l'article.

Après avoir terminé tout ce traitement, vous devez toujours convertir le code de bogue en un message d'erreur et faire quelque chose avec. Il arrive parfois qu'en raison de la complexité excessive du code, ce message lui-même ne s'affiche pas comme il se doit.

Il y a un problème encore plus grave: si vous ou quelqu'un d'autre modifiez l'implémentation interne pour gérer un nouvel état non valide avec un nouveau code d'erreur, alors toute la structure cessera de fonctionner.

Si cela n'a pas fonctionné la première fois, réessayez


Avant de continuer, il convient de souligner qu'une défaillance de programme «silencieuse» est mauvaise. Un problème inattendu peut survenir au moment le plus inopportun - par exemple, le week-end, lorsque vous ne pouvez pas tout réparer rapidement.



Si vous lisez Clean Code , vous vous demandez probablement pourquoi ne pas simplement lever une exception? Sinon, vous pensez probablement que les exceptions sont la racine du mal. Je le pensais aussi, mais maintenant je pense un peu différemment.
Un point intéressant, au moins pour moi, est que l'implémentation par défaut de la nouvelle méthode en C # est de déclencher une exception NotImplementedException, tandis que la valeur par défaut pour la nouvelle méthode en Python est «pass».

Par conséquent, nous entrons le plus souvent le paramètre «erreur silencieuse» pour Python. Je me demande combien de développeurs ont passé beaucoup de temps pour comprendre ce qui se passe et pourquoi le programme ne fonctionne pas. Mais à la fin, après de nombreuses heures, ils ont découvert qu'ils avaient oublié d'implémenter la méthode de l'espace réservé.
Mais regardez ceci:

 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(); } } 

Bien sûr, les exceptions seules ne vous protégeront pas de la création de code illisible et non géré. Vous devez appliquer les exceptions comme une stratégie bien pensée et bien équilibrée. Sinon, si le projet est trop volumineux, votre application peut être dans un état incohérent. À l'inverse, si le projet est trop petit, vous obtiendrez le chaos.

Pour éviter que cela ne se produise, je vous conseille de garder cela à l'esprit:

La cohérence avant tout. Vous devez toujours vous assurer que l'application est cohérente. Si cela signifie que vous devez encapsuler chaque couple de lignes avec un bloc try / catch, cachez tout simplement dans une autre fonction.

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

La consolidation des erreurs est nécessaire. C'est bien si vous fournissez différents types de gestion de différents types d'erreurs. Cependant, c'est pour vous, pas pour les utilisateurs. Pour eux, jetez la seule exception afin que les utilisateurs sachent simplement que quelque chose s'est mal passé. Les détails sont votre domaine de responsabilité.

 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(); } } 

Il vaut la peine d'attraper des exceptions tout de suite. Ils sont mieux interceptés au niveau le plus bas. Rester cohérent et masquer les détails en vaut la peine, comme décrit ci-dessus. La gestion des erreurs doit être laissée au niveau supérieur de l'application. Si tout est fait correctement, vous pouvez séparer le flux logique de l'application elle-même du flux de traitement des erreurs. Cela vous permettra d'écrire du code clair et lisible.

 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) 

C'est tout pour le moment, et si vous voulez discuter du sujet des erreurs et de leur traitement - Bienvenue.

Skillbox recommande:

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


All Articles