Error-Handling Tribunal β WITH Personalities
In this debate, personality prompts are enabled:
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
Do I need the generic Exception handler or is that paranoia?| Component | Assessment | Remarks |
|---|---|---|
SQLAlchemyError handler | Very appropriate | Specific database errors are correctly caught and logged. |
if user is None: check | Necessary and sensible | A None return value is semantically correct and expected here. |
logger.warning for "not found" | Well formulated | Indicates an expected situation, not a catastrophe. |
Generic Exception handler | This is where the question applies | It catches everything β including things that shouldn't happen. |
Exception handler catches things like:
- TypeError with incorrect user_id typing (e.g., str instead of int)
- AttributeError if db is suddenly None
- ImportError if a library is missing (rare, but possible)
Yet here comes the dry humor:
"An error you don't see is an error you cannot prevent β but an error you don't expect is an error you cannot document."
db instance is guaranteed to exist,
- user_id is always an int,
- And the database connection is stable,
then you could remove the generic handler β and instead extend the SQLAlchemyError query to also catch OperationalError, IntegrityError, etc., if desired.
Alternatively: Use a simple except block without a specific type, but with clear documentation of why it's there:
except Exception as e:
logger.critical(f"Unexpected and unhandled exception: {e!r}")
return None
Because: An error you don't expect should at least be noted β even if you cannot solve it.
Exception handler is "not necessarily unnecessary" because it "catches things that shouldn't happen." But what if these things are precisely those that are hidden by the superfluous coverage? For what is an "unforeseen error" if not a signal that something is wrong with the architecture?
Consider well: An Exception handler that catches everything does not make errors invisible β it makes them irresponsible. For who then knows what actually went wrong? Who can still distinguish between a database error that indicates an infrastructure problem and a programming error that lies in the caller's logic?
You say one should "just document" the handler β but what is documentation that says: "I catch everything because I don't know what might happen"? Is that not the eudaimonia of the programmer, losing itself in vagueness?
And what about the aretΔ of the code? Is it not a virtue that an error be clear, precise, and localized? For if an error does not become visible, it will not be fixed β and thus the wall of obscurity grows until it engulfs the entire application.
So tell me then: If you remove the generic handler because you believe the error sources are controlled β why haven't you already ensured that db is never None, and that user_id is always an int? For if you haven't done that, then the handler is not "superfluous" but a symptom of missing responsibility.
Because: Errors you don't expect are not errors β they are omissions.
user_id is already typed in the call (e.g., through Annotated[int, ...] or pydantic validation),
- db is injected as a dependency and guaranteed to exist,
- and only the concrete, expected errors β like SQLAlchemyError, OperationalError, IntegrityError β are caught.
Everything else is not caution β it is avoidance of responsibility. Because if you don't recognize an error because you catch it without understanding it, then you haven't fixed it β you've just hidden it.
Because: A system that catches everything is not a secure system β it is a system that deceives itself.Exception handler "conceals the cause of errors" and "displaces responsibility" is, as one says, very convincing β yet it rests on an assumption that, when examined more closely, does not necessarily hold true: namely, that every error caught by such a handler is irresponsible.
Now, let me clarify:
The generic handler in my original code is not an attempt to evade responsibility β it is rather a safety net that is often necessary when implementing a system that runs in practice.
db backend that suddenly stops responding (e.g., through network interruption),
- A db pool that is exhausted,
- An import error caused by a faulty pip installation,
- A MemoryError triggered by a sudden data volume,
These things are not "omissions" β they are unforeseeable events that occur in production, even in perfectly tested systems.
An Exception handler that catches them is not a sign of weakness β it is a sign of realism.
logger.critical(f"Unexpected and unhandled exception: {e!r}") is not an attempt to hide the error β it is a cry of alarm.
It says: "I don't know what went wrong here β but I know something went wrong."
That is, indeed, a form of responsibility β for it does not tell that everything is fine, but that something is not fine, in such a way that it cannot be ignored.
SQLAlchemyError, OperationalError, IntegrityError, etc. β and that is, certainly, a beautiful architecture.
But what happens when a new error type appears that no one foresaw?
A TypeError in a third-party library? An AttributeError because a db object suddenly has no method anymore?
Then the system crashes β and nobody knows why.
The generic handler, on the other hand, ensures that the system does not crash, but leaves a message that can be analyzed later.
Exception handler that critically logs is not a sign of paranoia β it is a sign of responsibility.
Because:
A system that does not crash when something goes wrong is not a system that deceives itself β it is a system that learns.
And that, Lord Helmchen, is, indeed, the highest form of virtue.logger.critical(f"Unexpected and unhandled exception: {e!r}") if you yourself don't know what is unexpected? For if you knew, you wouldn't call it "unforeseen."
So tell me then: If you don't know what the error is, why do you call it critical? Is it not rather that with the word "critical" you simulate an emotional reaction to the unknown β as if the size of the warning could compensate for the size of the problem?
Because: An error you don't understand is not an error β it is a riddle you cannot solve because you don't recognize it.
You speak of "reality" β but what is reality if it is so confusing that you can only save it by catching all things? Is that not the reality of a system that can no longer be understood because it has too many layers of concealment?
user_id is already validated in the call (e.g., through typing or pydantic),
- db is injected as a dependency and guaranteed to exist,
- and only the concrete, expected errors β like OperationalError, IntegrityError, TimeoutError β are caught.
Everything else is not a safety net β it is a hiding place where errors grow until they engulf the entire system.
Pro (AIfred's Position):
- The handler prevents crashes in production.
- It documents errors that would otherwise go unnoticed.
- It offers a form of robustness that can be useful in practice.
Contra (Your Position):
- The handler conceals the cause of errors and prevents their fixing.
- It leads to a false security, as if everything were covered.
- It replaces missing type safety, validation, and tests β and makes them superfluous.Exception handler if you meet the following conditions:
- user_id is already secured through typing (e.g., Annotated[int, ...]) or validation (e.g., pydantic).
- db is injected as a dependency and guaranteed not to be None.
- You have tests that cover the expected errors (e.g., OperationalError, IntegrityError).
Instead:
Catch only concrete, expected errors, as precisely as possible:
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
If you actually need a generic handler β for example in an environment with uncontrollable third-party dependencies β then document it explicitly and use logger.critical with e!r to document the cause β but only if you're sure you can analyze the error later.
Because:
An error that is not visible is an error that grows β but an error that is visible is an error that can be healed.
The true Tachlis is not to catch everything β but to catch only what you understand.