005
Improved exception handling in views
Lukas Juhrich
2021-12-21
Approved
The previous attempt to unify exception handling in blueprint views is done
via a web_execute
function, which acts as a proxy for a lib
function call:
result, success = web_execute(
lib_function,
"Success message!",
arg1, arg2='foo',
)
This is suboptimal in a few ways.
Firstly, having a boolean result value is comparatively unpythonic. Context managers and exception handling are builtin language features which are designed to solve the problem such a boolean variable does. In addition, using exceptions for control flow is perfectly admissable in python. Looking different from usual, explicit function calls, also makes it harder to reason about correctness and easier to overlook errors.
Secondly, it breaks code completion and signature checking of the function, both in IDEs and in Mypy. Absence of these protective mechanisms is a common source of error.
The web_execute
meta-function solves the following set of problems:
1. It flashes error messages when a specific subset of internal errors occur
2. consuming these internal exceptions
3. It rolls back the session
4. It flashes a success message if everything went great
Nota bene: Recently a PycroftException
has been introduced, making it easier
to handle all of the relevant exceptions instead of relying on a hard-coded list
to do that.
In addition, a useful pattern recently emerged in some parts of the web
code:
The final template rendering has been extracted into a local
default_response
functtion, which makes an early exit a la
return default_response()
possible.
This is a very useful pattern because it is straghtforward to reason about,
and does not introduce any unneccessary visual complexity to this kind of
branching, as e.g. an if/then/else/-block would.
It seems appropriate to split up the responsibilities of web_execute
like this:
should be handled with a context manager, intercepting certain kinds of exceptions (but not consuming then)
should be handled with a try: … except PycroftException: pass
block
should be handled in a context manager, for convenience the same as in 1.
should be issued by the view function explicitly, because passing the string to a sub-function doing the flashing instead has no benefit justifying this indirection.
An example would look like this:
@bp.route('/foo')
def view():
form = FooForm()
def default_response():
return render_template('generic-form.html', form=form)
if not form.is_submitted():
_fill_defaults(form)
return default_response()
if not form.validate():
return default_response()
try:
with handle_errors(session):
lib.foo(bar=form.bar.data)
session.commit()
except PycroftException:
return default_response()
flash("Good job!", 'success')
return redirect(url_for('.bar'))
Replace web_execute
usages by handle_error
usages
Promote early-exit pattern in functions which use one of the above constructs
Code completion for lib function calls will work again
View functions will become more readable