HowToCode: ErrorCodes, Exceptions, den User informieren, wenn etwas schief läuft – wie gehts?

image Usereingaben validieren, durch die Businesslogik schicken, dem Nutzer evtl. Fehler wieder anzeigen – fast jedes System macht sowas, doch wie macht man es "richtig"? Definiert man Errorcodes? Was ist mit Mehrsprachigkeit? In dem Post zeige ich meine Variante und bitte auch um Feedback :)

Ausgangssituation:
Ich arbeite gerade in einem Projekt mit einem Kollegen (worum es geht, werde ich später auch nochmal genauer beschreiben – spielt aber jetzt keine Rolle) und nutzen eine klassische 3-Schichten Architektur mitsamt ASP.NET MVC als Frontend.

image

Unser Business Layer haben wir, wie bei meinem ReadYou Projekt (was ich so nie beendet hab, aber die Ideen leben in dem anderen Projekt weiter) als "Service" Layer implementiert.

Service Layer?
Jeder Service hat ein Request und Response Objekt was jeweils rein bzw. raus geht. Dadurch hab ich dies etwas gekapselt und kann neben dem eigentlich Wert noch weitere Information rausgeben.

image

Warum und wie ich das mache, habe ich in diesem Post auch noch näher erklärt.

Ich hab ein kleines Demoprogramm entwickelt, welches stark vereinfacht (keine Interfaces, kein DI, keine Tests) zeigt, wie meine Variante momentan ist.
Ich bin mir nicht ganz sicher, ob es so toll ist – daher bitte ich im Feedback, wie ihr sowas löst.

Demoprojekt:

image

ConApp: Frontend

Model: Das Application Model – umfasst hier nur einen User

Repository: Datenzugriffsschicht – hier werden ein paar User zurückgegeben

Service: Business Logik

 

 

 

 

Das Model & das Repository:

Das Model ist sehr einfach gehalten:

    public class User
    {
        public string Name { get; set; }
        public Guid Id { get; set; }
    }

Das UserRepository erstellt mir einfach 5 User:

namespace Repository
{
    public class UserRepository
    {
        public IList<User> GetUsers()
        {
            List<User> returnList = new List<User>();
            returnList.Add(new User() { Id = Guid.NewGuid(), Name = "Tester0" });
            returnList.Add(new User() { Id = Guid.NewGuid(), Name = "Tester1" });
            returnList.Add(new User() { Id = Guid.NewGuid(), Name = "Tester2" });
            returnList.Add(new User() { Id = Guid.NewGuid(), Name = "Tester3" });
            returnList.Add(new User() { Id = Guid.NewGuid(), Name = "Tester4" });

            return returnList;
        }
    }
}

Der Service:

Der Service enthält generische Request und Response Klassen:

Request:

    public class ServiceRequest<T>
    {
        public T Value { get; set; }
    }

Response (inklusive 2 Enums) :

    public class ServiceResponse<T>
    {
        public ServiceResult Result { get; set; }
        public ErrorCodes ErrorCodes { get; set; }
        public Exception Exception { get; set; }
        public T Value { get; set; }
    }

    /// <summary>
    /// Usererror Codes?
    /// </summary>
    public enum ErrorCodes
    {
        InvalidName,
        NothingFound
    }

    /// <summary>
    /// Succeeded = Everything worked
    /// Failed = Userinput incorrect or business logic has detected an error
    /// Fatal = exception occured (e.g. db down)
    /// </summary>
    public enum ServiceResult
    {
        Succeeded,
        Failed,
        Fatal
    }

Hier eine kleine Erklärung:

Das ServiceResult gibt einfach an, ob der Call geklappt ("Succeeded") oder fehlgeschlagen ist. Wenn er fehlgeschlagen ist, dann kann das aufgrund von Fehleingaben sein, oder weil die Business Logik sagt, dass z.B. der Login inkorrekt ist ("Failed"). Die verschiedenen Userfehler gründe speichere ich in einem ErrorCode Enum.

Ein "Fatal Error" wäre, wenn tatsächlich das Repository z.B. eine SqlExpcetion werfen würde. Diese Exception speicher ich in einer Variable.

Code smell

So ganz bin ich mit der momentanen Lösung allerdings nicht zufrieden – ein ErrorCodes Enum? Das wird sicher irgendwann ziemlich groß.

Beim Entwickeln der Anwendung fiel mir bereits auf, dass man die Exceptions auch in die nächste Schicht über ein throw befördern könnte – ist das gut oder eher schlecht? Wenn ich es im Service mache, dann brauch ich die Exceptionbehandlung nicht im Frontend zu erledigen, sondern kann ein "Fatal" weitergeben und dem Nutzer sagen, dass die Anwendung gerade einen Fehler verursacht hat.

Soweit, so gut – hier jetzt der UserSerivce

Der UserService:

public class UserService
    {
        public ServiceResponse<User> GetUser(ServiceRequest<string> userName)
        {
            // simulate Usererror
            if (String.IsNullOrEmpty(userName.Value))
            {
                return new ServiceResponse<User>()
                {
                    Result = ServiceResult.Failed,
                    ErrorCodes = ErrorCodes.InvalidName
                };
            }

            // simulate Systemerror
            if (userName.Value == "Tester0")
            {
                return new ServiceResponse<User>()
                {
                    Result = ServiceResult.Fatal,
                    Exception = new ArgumentException()
                };
                // or should I throw it???
            }

            // everything works
            UserRepository rep = new UserRepository();
            User result = rep.GetUsers().ToList().Where(x => x.Name == userName.Value).SingleOrDefault();
            // nothing found
            if (result == null)
            {
                return new ServiceResponse<User>() { Result = ServiceResult.Failed, ErrorCodes = ErrorCodes.NothingFound };
            }
            else
            {
                return new ServiceResponse<User>() { Value = result, Result = ServiceResult.Succeeded };
            }
        }
    }

Hier hab ich mal die verschiedenen Fälle implementiert.

Als erstes prüft man, ob der Nutzer überhaupt was eingegeben hat, wenn nicht, dann gibt er "Failed" mit dem passenden ErrorCode zurück.

Um ein Exception (also einen Fatal Error) zu simulieren, übergibt man Tester0. In diesem Fall reicht man die Exception weiter. Hier war ich mir selber auch unsicher – sollte man die Exception nach oben befördern?

Ich hab mich für diese Variante entschieden, da der Service bei mir mit der Kern ist. Hier kann der Fehler entsprechend behandelt werden und der, der die Methode Aufruft, bekommt die Exception mit, die es ausgelöst hat – um es z.B. zu loggen.

Danach ruf ich das Repository auf und wenn etwas gefunden wurde, dann gibt es ein "Succeeded", ansonsten ein "Failed".

ConApp

class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Enter Username");
            Console.WriteLine("(Tester0 == Systemerror)");
            Console.WriteLine("(enter nothing == Usererror)");

            string username = Console.ReadLine();

            UserService srv = new UserService();
            ServiceResponse<User> result = srv.GetUser(new ServiceRequest<string>() { Value = username });

            if (result.Result == ServiceResult.Succeeded)
            {
                Console.WriteLine("Service Call Succeeded");
                Console.WriteLine(result.Value);
            }

            if (result.Result == ServiceResult.Failed)
            {
                Console.WriteLine("Service Call Failed");
                Console.WriteLine(result.ErrorCodes.ToString());
            }

            if (result.Result == ServiceResult.Fatal)
            {
                Console.WriteLine("Service Call Fatal Error");
                Console.WriteLine(result.Exception.ToString());
            }

            Console.ReadLine();
        }
    }

Hier sieht man das ganze mal in Action. Durch den Einsatz der ErrorCodes könnte man an dieser Stelle sogar noch einen anderen Service aufrufen, der den Code in ein passenden, Userfreundlichen (in in seiner Sprache geschriebenen) Satz umwandeln könnte.

Kann man sich die ErrorCodes nicht sparen?

Natürlich könnte ich auch Exceptions dafür nehmen, allerdings steht dort in den Messages nicht wirkich Userfreundliche Informationen drin. Ich wollte auch eine Art Trennung zwischen "Systemfehlern" und "Userfehlern" dadurch erzwingen.

Die Applikation kann man natürlich runterladen. Ich würde mich freuen wenn ihr Feedback darauf gebt – sind Enums die Erfüllung? Geht es nicht "schöner"? Was haltet ihr von dem Gesamtkonstrukt? Wenn ihr selber ein Demoprojekt habt, dann schickt es an meine Emailadresse (siehe Impressum) – für Anregung bin ich immer dankbar :)

[ Download Demoapplikation ]


Kick It auf dotnet-kicks.de
Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf dem Laufenden bleiben willst, abonniere unseren RSS Feed oder folge uns auf Twitter.

About the author

Written by Robert Mühsig

Robert Mühsig (@robert0muehsig) ist Webentwickler und beschäftigt sich mit Web-Frameworks (vor allem dem ASP.NET MVC Framework) und scheut sich auch nicht vor Javascript. Ansonsten bloggt er über all jene Probleme, die ihm über den Weg laufen. Seit 2008 ist er Microsoft MVP für ASP.NET und er arbeitet bei der T-Systems Multimedia Solutions GmbH in Dresden. Treffen kann man ihn online via Twitter (@robert0muehsig) oder dieser Seite oder bei der .NET User Group Dresden.

15 Responses

  1. Ich habe dir keine Antworten, bin aber auf diese gespannt. Das sind Fragen die mich auch noch plagen.

    Reply
  2. Hi Robert,ich stand vor 3-4 Wochen genau vor dem selben Problem und hab mich gegen die ErrorCode und für customized Exceptions entschieden.

    Meine Architektur ähnelt der in deiner Demo Anwendung (DomainModel -> Service -> WebUI).
    Wie man in deiner Demo Anwendung sieht, hast du für die Falscheingabe des Usernamens ein ErrorCode. Bei einem umfangreicherem Model müsstest du für jede Usereingabe (email, pwd etc) jeweils einen ErrorCode einpflegen.
    Sehr schnell ist dieses Enum dann ziemlich aufgebläht. Und das war für mich ein Argument gegen die ErrorCodes.Ich habe in meiner Applikation für Fehler in der Business-Schicht die DomainModelException und für Fehler in der Service-Schicht ein ServiceException (oder DatabaseException). Da werden die ganzen Fehler mit der Kommunikation zur Datenbank geworfen.
    Die Exceptions werden bei mir schon lokalisiert durch die Schichten bis hin zur WebUI durchgereicht.
    Für mich war es wichtig wirklich aussagekräftige und lokalisierte Fehlermeldungen zu generieren und diese Lösung erschien mir am sinnvollstens.Eine Recherche im Internet ergab leider nicht wirklich brauchbares. Hier wären Best Practices sicherlich nicht verkehrt.
    Ich würd mich freuen, wenn andere Leser ihre Vorgehensweise auch darlegen könnten.Grüße Mariusz

    PS: Aus Opera heraus kann ich bei dir nicht kommentieren. Kriege jedesmal einen Error: please type a comment.

    Reply
  3. Also ich habe mir Derartiges abgewöhnt, das ist zu teuer. In Webanwendungen fange ich überhaupt keine Exceptions mehr innerhalb ab, sondern lasse alles durchlaufen, fange es dann in Application_Error() ab, logge den Fehler, verschicke ggf. ne Benachrichtigung und dann geht’s für den User ab zur error.aspx, in dem ihm eine allgemein Fehlermeldung in seiner Sprache angezeigt wird- denn Fehlerdetails gehen ihn nichts an.

    Reply
  4. An diesem Thema habe ich in der letzten Zeit gearbeitet. Allerdings mit Verwendung von WCF und der Enterprise Library. Vorweg, die Error Codes finde ich nicht gut, eine Kategorisierung wie ServiceResult könnte allerdings hilfreich sein. Welche Fehler treten den auf dem Server auf? Fehler bei der Validierung, Geschäftsregelverletzungen, Authorisierungsfehler, Unerwartete Fehler wie SQL Fehler, Timeouts oder durch gleichzeitige Zugriffe… In meinem Projekt werden die generierten Exceptions direkt übernommen oder in spezielle ExceptionDetails für FaultExceptions verpackt. Der Client verwendet eine Bibliothek diese Strukturen auszuwerten. Wer möchte schon ewig lange catch, catch, catch… Anweisungen verwenden. Gerade bei komplexen Fehlern muss die relevante Fehlerinformation ermittelt werden um diese dem Anwender zu präsentieren. Auch im Zusammenhang mit der Validierung (Enterprise Library) wird hier eine Meldung generiert, die der Anwender verstehen kann. Der Entwickler hat erweiterte Möglichkeiten. Ich möchte demnächst einige Beiträge zu diesen Themen zu veröffentlichen. Weitere Meinungen zu diesem Beitrag würden mich natürlich auch interessieren.

    Reply
  5. Ich stehe wie soviele vor ähnlichen Problemen, nur das ich primär WindowsForms entwickle. Momentan handhabe ich es so, das ich eine Exception bis zur GUI zurückwerfe mit einem individualen Fehlertext für den Nutzer und in der InnerException habe ich die eigentliche Exception abgelegt, welche mir dann weggeschrieben wird in ein XML-Log, Textlog oder EventLog.

    Bin aber auch noch nicht so richtig zufrieden damit, hab aber von vielen gehört das die Enterprise Libary da gut aushelfen soll. Leider hab ich noch keinerlei Erfahrung damit sammeln können :(

    Gruß
    Robert

    Reply
  6. Ich halte es vorallem bei Webanwendungen kritisch, die Exception bis ins Frontend zu "schleifen". Die Exception muss samt dem Stack ins Logfile, aber das Frontend sollte nur zu hören bekommen, dass etwas schief gelaufen ist.
    Man sollte dem Frontend meiner Ansicht nach keinen zu tiefen "Einblick" ins System gewähren…

    Bei Fehlern, welche userbedingt sind (z.B. Fehleingaben etc.) sollte man nur darauf hinweisen, dass in Feld XY ein Fehler ist. Mehr muss das Frontend meiner Meinung nach nicht wissen.

    Die Frage ist nur, in welcher Art man dem Frontend den Fehler begreiflich macht. Schreibt man es in die Message einer customized Exception oder gibt man hier nur einen ErrorCode an das Frontend und dieses generiert entsprechend dem ErrorCode die passende Nachricht!?!?

    Reply
  7. Aso,
    bei mir kriegt der Benutzer eine benutzerdefinierte Fehlermeldung, wie du oben erwähnt hast, was er in welchem Feld falsch gemacht hat.
    Den Stacktrace kriegt der Benutzer nie zu Gesicht, genau so die DatabaseExceptions. Hier wird der Benutzer drauf aufmerksam gemacht, das im System etwas schief gelaufen ist alla Twitter.

    Reply
  8. Error Codes haben für mich den faden Beigeschmack von strings im Code.

    Man muss sie pflegen, erweitern und kann sie nur mit langen if/else Konstrukten auswerten.Ich würde mir für jede Schicht eigene Exception Typen erstellen, die wenn sie die Schicht verlassen in für die andere Schicht verständliche Exceptions gewrappt werden.Bsp: Eine SQL Exception tritt in deinem DataLayer auf. Der Data Layer behandelt sie (vielleicht muss ein Rollback durchgeführt werden oder ähnliches), wrapt sie dann in eine DataAccessException und gibt sie an den Servicelayer weiter.Der Servicelayer kann dann seinerseits die Exception behandeln (loggen z.B.) und an die UI eine passende ServiceLayerException weiterleiten.Diese kann dann die Exception fangen und den Benutzer entsprechend benachrichtigen ("Ein interner Serverfehler ist aufgetreten", wie auch immer).So hast du weder ewig lange try, catch, catch, catch…, noch wartungsintensive if/else Konstrukte. Wenn ein neuer Fehler behandelt werden muss betrifft dieser nur die Komponente, in der er auftritt.

    Reply
  9. Also das Thema Fehlerbehandlung ist das Thema, das bei den meisten Musterlösungen immer stiefmütterlich behandelt, wenn nicht gar weggelassen wird. Kann man dafür eigentlich ein allgemeines Pattern definieren? Wenn ja, wäre das doch wirklich mal notwendig oder? Damit nicht immer wieder die Überlegung ansteht, bis wohin gebe ich die Exceptions direkt weiter und ab wo erzeuge ich meine eigenen oder gebe es als Returnwert zurück.Wäre doch schön, wenn auch diese Thema mal in den Patterns und Practises von MS einen ausführlichen Platz finden könnte. Der Application Block dafür ist ja da. Aber passt der auch immer?

    Reply
  10. Ich mache es genauso oder zumindest ähnlich wie Sandal und habe damit bisher gute Erfahrungen gemacht.

    Für DataLayerExceptions habe ich ein Enum, damit ich den Fehlertyp im BusinessLayer einfacher auswerten kann. Die SqlException ist dann in der InnerException und wird vom BusinessLayer geloggt. Das Enum habe ich verwendet, da es beim Datenzugriff immer die gleichen Fehler gibt, die unabhängig von der BL sind.

    Im DataLayer habe ich eine Helper-Klasse, die z.B. eine SqlException automatisch in eine DataLayerException umwandelt. Das spart viel Code.

    Der BusinessLayer gibt dann wiederum eine UserException an das Frontend zurück. Darin befindet sich dann auch eine lokalisierte Fehlermeldung, die direkt an den Benutzer ausgegeben werden kann.

    Reply
  11. Hallo zusammen,

    diese Gedanken mache ich mir auch bei jedem neuen Projekt. Ziemlich gut fande ich in einem Buch mal den Ausdruck das Exceptions eben "Ausnahmen" sind und man damit möglichst nicht den Programmfluss steuern soll. Also man sollte einfach mal annehmen das die Exception in 0.01% der Fälle auftritt – wenn das nicht so ist dann hat man die Exception für den Programmfluss misbraucht.

    Heißt also wie auch den Tenor hier im Thread angibt das es wohl am Besten ist die Exceptions weiter zu werfen oder werfen zu lassen und kurz vor dem Userinterface zu loggen. Den User interessiert es doch eh nicht warum was nicht geht sondern nur das etwas nicht geht.

    Allerdings stimme ich zu das es hier viele verschiedene Fälle gibt für die eine Art Best Practices nicht schlecht wäre. Falls hierzu jemand ne gute Seite findet wäre ich auch sehr interessiert daran.

    Reply

Comment on this post

Letzte Posts

  • Carriage Return / Neue Zeile in Textareas

    Eine kleine Aufgabe: Jede neue Textzeile (Carriage Return/Wenn man Enter drückt ) in einer Textarea soll ein Element in einer Auflistung sein – wie mach ich das jetzt am einfachsten? Eigentlich ein grundlegendes Element im Web und der Nutzer macht bewusst Absätze – daher wäre es nur gerecht, wenn man das auch entsprechend würdigt. Kleine ...

  • image.png
    Doom, Quake, Wolfenstein & co. Source Code auf GitHub

    id Software, die Macher von Doom, Quake, Wolfenstein & co., stellen regelmäßig ihre älteren Spieltitle als Open Source zur Verfügung. Das Ganze runterzuladen fand ich bisher immer recht mühselig, allerdings gibt es seit kurzer Zeit die Sourcen auch auf GitHub. Darunter Spiele wie Doom 3, Quake 3, Wolfenstein für iOS. Wer also schon immer mal ...

  • image.png
    Twitter Bootstrap 2.0 released & “Release Präsentation”

    Wie bereits vom Twitter Bootstrap Team angekündigt wurde offiziel die Version 2.0 des UI Toolskits “Twitter Bootstrap” veröffentlich. Zudem wurden die Slides, welche bei der Release Party gezeigt wurden auch veröffentlicht: Downloads finden sich auf der Twitter Bootstrap Seite auf GitHub. Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf ...

  • image.png
    Javascript zu Dart Translator

    Dart, Google Javascript Alternative, wurde vor ein paar Monaten vorgestellt und die Webentwickler Szene ist noch etwas gespalten, ob Dart nun überflüssig ist oder einfach nur cool und längst überfällig ist. Um die Sprache näher zu erläutern hat Google die grundlegenden Javascript Basics nach Dart übersetzt. Das Ergebnis ist der “Translator”. Der Name mag momentan ...

  • Twitter Bootstrap 2.0–“Beta”

    Twitter Bootstrap, ein UI-Toolkit für Web-Applikationen von Twitter, erscheint (wie bereits berichtet) demnächst in der Version 2.0. Der offizielle Release ist am 31. Januar, allerdings beginnt jetzt laut Mark Otto (einer der Hauptentwickler von Twitter Bootstrap) die intensive Test-Phase. Das heisst, das es nun offiziel auch die 2.0 Dokumentation online gibt. Im Vergleich zur aktuellen ...

Support us!

Facebook