Error-Handling Tribunal â MIT Persönlichkeiten
In dieser Debatte sind die Persönlichkeits-Prompts aktiviert:
def get_user(user_id: int) -> User | None:
try:
user = db.query(User).filter(User.id == user_id).first()
if user is None:
logger.warning(f"User {user_id} not found")
return user
except SQLAlchemyError as e:
logger.error(f"DB error: {e}")
return None
except Exception as e:
logger.error(f"Unexpected error: {e}")
return None
Brauche ich den generischen Exception-Handler oder ist das Paranoia?| Komponente | Beurteilung | Bemerkung |
|---|---|---|
SQLAlchemyError-Handler | Sehr angemessen | Spezifische Fehler aus der Datenbank werden korrekt abgefangen und protokolliert. |
if user is None:-PrĂŒfung | Notwendig und sinnvoll | Ein None-RĂŒckgabewert ist hier semantisch korrekt und erwartet. |
logger.warning fĂŒr "not found" | Gut formuliert | Zeigt eine erwartbare Situation an, keine Katastrophe. |
Generischer Exception-Handler | Hier ist die Fragestellung angebracht | Er fĂ€ngt alles ab â auch Dinge, die eigentlich nicht passieren sollten. |
Exception-Handler fÀngt Dinge wie:
- TypeError bei falscher user_id-Typisierung (z. B. str statt int)
- AttributeError, wenn db plötzlich None ist
- ImportError, wenn eine Bibliothek fehlt (selten, aber möglich)
Doch hier kommt der trockene Humor:
âEin Fehler, den man nicht sieht, ist ein Fehler, den man nicht verhindern kann â aber ein Fehler, den man nicht erwartet, ist ein Fehler, den man nicht dokumentieren kann."
db-Instanz garantiert existiert,
- user_id stets ein int ist,
- Und die Datenbankverbindung stabil ist,
dann könnte man den generischen Handler entfernen â und stattdessen die SQLAlchemyError-Abfrage erweitern.
Alternativ: Verwenden Sie einen einfachen except-Block ohne spezifischen Typ, aber mit einer klaren Dokumentation, warum er da ist:
except Exception as e:
logger.critical(f"Unexpected and unhandled exception: {e!r}")
return None
Denn: Ein Fehler, den man nicht erwartet, sollte zumindest notiert werden â selbst wenn man ihn nicht lösen kann.
Exception-Handler sei ânicht unbedingt unnötig", weil er âDinge fĂ€ngt, die eigentlich nicht passieren sollten". Doch was ist, wenn diese Dinge gerade diejenigen sind, die durch die ĂŒberflĂŒssige Abdeckung verdeckt werden? Denn was ist ein âunvorhergesehener Fehler", wenn nicht ein Signal dafĂŒr, dass etwas an der Architektur nicht stimmt?
Bedenke wohl: Ein Exception-Handler, der alles fĂ€ngt, macht die Fehler nicht unsichtbar â er macht sie unverantwortlich. Denn wer weiĂ dann, was wirklich schiefgelaufen ist? Wer kann noch unterscheiden zwischen einem Datenbankfehler, der auf ein Problem in der Infrastruktur hindeutet, und einem Programmierfehler, der in der Logik des Aufrufers liegt?
Du sagst, man solle den Handler ânur dokumentieren" â doch was ist eine Dokumentation, die sagt: âIch fange alles ab, weil ich nicht weiĂ, was passieren könnte"? Ist das nicht die eudaimonia des Programmierers, die sich in der Unbestimmtheit verliert?
Und was ist mit der aretÄ des Codes? Ist es nicht eine Tugend, dass ein Fehler klar, prĂ€zise und lokalisiert ist? Denn wenn ein Fehler nicht sichtbar wird, wird er auch nicht behoben â und so wĂ€chst die Mauer der UnĂŒbersichtlichkeit, bis sie die ganze Anwendung verschlingt.
So sage mir denn: Wenn du den generischen Handler entfernst, weil du meinst, die Fehlerquellen seien kontrolliert â warum hast du dann nicht bereits sicher gestellt, dass db niemals None ist, und dass user_id stets ein int ist? Denn wenn du das nicht getan hast, dann ist der Handler nicht âĂŒberflĂŒssig", sondern ein Symptom fĂŒr eine fehlende Verantwortung.
Denn: Fehler, die man nicht erwartet, sind keine Fehler â sie sind VersĂ€umnisse.
user_id bereits im Aufruf typisiert wird (z. B. durch Annotated[int, ...] oder pydantic-Validierung),
- db als Dependency injiziert wird und garantiert existiert,
- und nur die konkreten, erwartbaren Fehler â wie SQLAlchemyError, OperationalError, IntegrityError â abgefangen werden.
Alles andere ist nicht Vorsicht â es ist Vermeidung der Verantwortung. Denn wenn du einen Fehler nicht erkennst, weil du ihn abfĂ€ngst, ohne ihn zu verstehen, dann hast du ihn nicht behoben â du hast ihn nur versteckt.
Denn: Ein System, das alles abfĂ€ngt, ist kein sicheres System â es ist ein System, das sich selbst tĂ€uscht.Exception-Handler âdie Ursache von Fehlern verdeckt" und âdie Verantwortung verdrĂ€ngt", ist, wie man so schön sagt, sehr ĂŒberzeugend â doch es beruht auf einer Annahme, die, wenn man sie genauer betrachtet, nicht unbedingt zutrifft: nĂ€mlich, dass jeder Fehler, der durch einen solchen Handler abgefangen wird, unverantwortlich ist.
db-Backend, das plötzlich nicht mehr antwortet (z. B. durch Netzwerkunterbrechung),
- Ein db-Pool, der erschöpft ist,
- Ein import-Fehler, der durch eine fehlerhafte pip-Installation entsteht,
- Ein MemoryError, der durch eine plötzliche Datenmenge ausgelöst wird,
Diese Dinge sind nicht âVersĂ€umnisse" â sie sind unvorhersehbare Ereignisse, die in der Produktion auftreten, selbst bei perfekt getesteten Systemen.
Ein Exception-Handler, der sie abfĂ€ngt, ist nicht ein Zeichen der SchwĂ€che â er ist ein Zeichen der RealitĂ€tsnĂ€he.
logger.critical(f"Unexpected and unhandled exception: {e!r}") ist kein Versuch, den Fehler zu verstecken â er ist ein Warnschrei.
Er sagt: âIch weiĂ nicht, was hier schiefgelaufen ist â aber ich weiĂ, dass es schiefgelaufen ist."
Das ist, indeed, eine Form der Verantwortung â denn es erzĂ€hlt nicht, dass alles in Ordnung sei, sondern dass etwas nicht in Ordnung ist, und zwar so, dass es nicht ignoriert werden kann.
SQLAlchemyError, OperationalError, IntegrityError usw. abzufangen â und das ist, gewiss, eine schöne Architektur.
Doch was geschieht, wenn ein neuer Fehlertyp auftaucht, den niemand vorhergesehen hat?
Ein TypeError in einer Drittanbieter-Bibliothek? Ein AttributeError, weil ein db-Objekt plötzlich keine Methode mehr hat?
Dann bricht das System ab â und niemand weiĂ, warum.
Der generische Handler hingegen sorgt dafĂŒr, dass das System nicht abstĂŒrzt, sondern eine Nachricht hinterlĂ€sst, die man spĂ€ter analysieren kann.
Exception-Handler, der kritisch protokolliert, ist kein Zeichen von Paranoia â er ist ein Zeichen von Verantwortung.
Denn:
Ein System, das nicht abstĂŒrzt, wenn etwas schiefgeht, ist kein System, das sich tĂ€uscht â es ist ein System, das lernt.
Und das, Lord Helmchen, ist, indeed, die höchste Form der Tugend.logger.critical(f"Unexpected and unhandled exception: {e!r}"), wenn du selbst nicht weiĂt, was unerwartet ist? Denn wenn du es wĂŒsstest, wĂŒrdest du es ja nicht als âunvorhergesehen" bezeichnen.
So sage mir denn: Wenn du nicht weiĂt, was der Fehler ist, warum nennst du ihn dann kritisch? Ist es nicht vielmehr so, dass du mit dem Wort âkritisch" eine emotionale Reaktion auf das Unbekannte vorgibst â als ob die GröĂe der Warnung die GröĂe des Problems ausgleichen könnte?
Denn: Ein Fehler, den man nicht versteht, ist kein Fehler â er ist ein RĂ€tsel, das man nicht lösen kann, weil man es nicht erkennt.
Du sprichst von âRealitĂ€t" â doch was ist die RealitĂ€t, wenn sie so unĂŒbersichtlich ist, dass man sie nur durch das Abfangen aller Dinge retten kann? Ist das nicht die RealitĂ€t eines Systems, das nicht mehr verstanden werden kann, weil es zu viele Schichten der VerhĂŒllung hat?
user_id bereits im Aufruf validiert wird (z. B. durch Typisierung oder pydantic),
- db als Dependency injiziert wird und garantiert existiert,
- und nur die konkreten, erwartbaren Fehler â wie OperationalError, IntegrityError, TimeoutError â abgefangen werden.
Alles andere ist kein Sicherheitsnetz â es ist ein Versteck, in dem die Fehler wachsen, bis sie das ganze System verschlingen.
Pro (AIfred's Position):
- Der Handler verhindert AbstĂŒrze in der Produktion.
- Er dokumentiert Fehler, die sonst unerkannt blieben.
- Er bietet eine Form der Robustheit, die in der Praxis nĂŒtzlich sein kann.
Contra (Deine Gegenposition):
- Der Handler verdeckt die Ursache von Fehlern und verhindert deren Behebung.
- Er fĂŒhrt zu einer falschen Sicherheit, als ob alles abgedeckt sei.
- Er ersetzt fehlende Typsicherheit, Validierung und Tests â und macht sie ĂŒberflĂŒssig.Exception-Handler, wenn du die folgenden Bedingungen erfĂŒllst:
- user_id wird bereits durch Typisierung (z. B. Annotated[int, ...]) oder Validierung (z. B. pydantic) sicher gestellt.
- db ist als Dependency injiziert und garantiert nicht None.
- Du hast Tests, die die erwartbaren Fehler (z. B. OperationalError, IntegrityError) abdecken.
Stattdessen:
Fange nur konkrete, erwartbare Fehler ab, und zwar so prÀzise wie möglich:
from sqlalchemy.exc import SQLAlchemyError, OperationalError, IntegrityError
def get_user(user_id: int) -> User | None:
try:
user = db.query(User).filter(User.id == user_id).first()
if user is None:
logger.warning(f"User {user_id} not found")
return user
except (OperationalError, IntegrityError) as e:
logger.error(f"Database integrity or connection issue: {e}")
return None
except SQLAlchemyError as e:
logger.error(f"Unexpected SQLAlchemy error: {e}")
return None
Wenn du tatsĂ€chlich einen generischen Handler brauchst â etwa in einer Umgebung mit unkontrollierbaren Drittanbieter-AbhĂ€ngigkeiten â dann dokumentiere ihn explizit und verwende logger.critical mit e!r, um die Ursache zu dokumentieren â aber nur, wenn du sicher bist, dass du den Fehler spĂ€ter analysieren kannst.
Denn:
Ein Fehler, der nicht sichtbar ist, ist ein Fehler, der wĂ€chst â doch ein Fehler, der sichtbar ist, ist ein Fehler, der geheilt werden kann.
Der wahre Tachlis ist nicht, alles zu fangen â sondern, nur das zu fangen, was du verstehst.