How to access ui when handling exceptions with on_exception event #2026
-
QuestionI'm building a desktop app with NiceGUI, the app talks to the REST API. To handle HTTP errors, my client wrapper raises custom exceptions. Then I catch those exceptions in It works nicely when the calls are made AFTER the page gets rendered, e.g. on button click. But I don't know how to make the exception handler work when I need to make the REST API call when the page loads. It throws the following error:
To be honest, I'm absolutely clueless of what slot is, the word "slot" does not even appear on the main documentation page. There are some examples of add_slot function for different ui elements but it's totally confusing due to lack of meaningful information and/or links. Full example illustrating the problem: from nicegui import app, ui
class RESTPermissionError(Exception):
pass
class RESTNotFoundError(Exception):
pass
class RESTServerError(Exception):
pass
class RESTAuthenticationError(Exception):
pass
def app_exception(ex):
"""
We want to handle all REST API exceptions consistently across all pages in the app.
"""
if isinstance(ex, RESTPermissionError): # HTTP 403 Forbidden
ui.notify("You do not have permission to perform this action", type="warning")
elif isinstance(ex, RESTNotFoundError): # HTTP 404 Not Found
ui.notify("Resource not found or you have no permission to access it.", type="warning")
elif isinstance(ex, RESTServerError): # HTTP 500 Internal Server Error
ui.notify("Server error. Please try again later.", type="negative")
elif isinstance(ex, RESTAuthenticationError): # HTTP 401 Unauhtorized
# User was logged out. We remove their auth auth_token (log them off) and redirect them back to the login page
# This is just an example, but you get the idea....
app.storage.user.pop('auth_token')
ui.notify("Your session expired. Please log back in.", type="ongoing")
ui.open('/login') # just an example
app.on_exception(app_exception)
@ui.page('/')
async def main_page() -> None:
def get_data_from_rest_api_error():
raise RESTPermissionError # Simulate API client wrapper throwing exception on getting 403 from the server
def get_data_from_rest_api_success():
data_from_server = "here be data from da server" # Simulate API client wrapper returning data successfully
data_presentation.text = data_from_server # we can bind UI element with data returned from server
ui.label('Welcome to the main page! It allows to get data via HTTP REST API call:')
ui.button("Get data (error)", on_click=get_data_from_rest_api_error)
ui.button("Get data (success)", on_click=get_data_from_rest_api_success)
data_presentation = ui.label()
ui.link('Work nicely, huh? Now check out the other page', target='/other')
@ui.page('/other')
async def main_page() -> None:
def get_data_from_rest_api_error():
raise RESTPermissionError # Simulate API client wrapper throwing exception on getting 403 from the server
def get_data_from_rest_api_success():
data_from_server = "here be data from da server" # Simulate API client wrapper returning data successfully
data_presentation.text = data_from_server
ui.label('Welcome to the other page! This one wants to make request to the REST API immediately.')
data_presentation = ui.label()
get_data_from_rest_api_error() # let's suppose something went wrong when fetching the data
# get_data_from_rest_api_success() # uncomment this to simulate the happy scenario
ui.run(
title="My App",
port=8502,
native=True,
reload=True,
window_size=(1920, 1200),
storage_secret='THIS_NEEDS_TO_BE_CHANGED'
) I suppose there should be some kind of "page loaded" event where I could run the code to get data from the server AFTER the page has rendered? But can't find any info if such event is available? |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 5 replies
-
Have you looked into the events? |
Beta Was this translation helpful? Give feedback.
-
The problem with Apart from the technical details, it boils down to this: An app exception can happen anywhere in the code. It could also occur in a REST endpoint which has nothing to do with NiceGUI. If the exception handler would want to show a notification or open a new URL, it is unclear which browser should receive these commands. If you simply call A workaround could be to simply notify all clients, like I described here. But this is probably not what you want. Instead you can catch exception within the page function for UI-related handling, and use the global exception handler for things like logging which don't need a UI context. |
Beta Was this translation helpful? Give feedback.
-
Oh, I just found a pretty handy workaround: from nicegui import Client, app, ui
class UiException(Exception):
pass
def handle_exception(exception: Exception):
if isinstance(exception, UiException):
client, = exception.args
with client:
ui.notify('An error occurred', type='negative')
app.on_exception(handle_exception)
@ui.page('/')
async def index(client: Client):
ui.label('Hello World!')
await client.connected()
raise UiException(client)
ui.run() The custom |
Beta Was this translation helpful? Give feedback.
-
@falkoschindler would y'all be open to a PR to improve this behavior somewhat inside the In my experience, if an error occurs while handling a callback, there's probably going to be a client associated with those EventArguments. And so a global error handler could be registered that would be able to access the client directly, and possibly put up a very big, scary error message saying "something went wrong" - and this wouldn't require individual developers to remember to wrap Every Single Even though a more polished UI might well want to wrap every single callback with some kind of error handler, it seems reasonable to give teams a method for globally preventing 'silent errors' - errors that are logged but (incorrectly) do not appear to affect the behavior of the pages themselves. I'm imagining something like: def handle_event()...
....
try:
await result
except Exception as e:
e._nicegui_event_arguments = arguments
core.app.handle_exception(e)
if core.loop and core.loop.is_running():
background_tasks.create(wait_for_result(), name=str(handler))
else:
core.app.on_startup(wait_for_result())
except Exception as e:
e._nicegui_event_arguments = arguments
core.app.handle_exception(e) and then an application could register a handler that does something like: def handle_exception(exc):
if hasattr(exc, "_nicegui_event_arguments"):
with exc._nicegui_event_arguments.client:
with ui.dialog() as dialog, ui.card():
dialog.props("no-esc-dismiss no-backdrop-dismiss")
ui.label(f"A very bad error occurred: {exc}")
ui.label(traceback.format_exc()).classes(
"bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative"
).style("white-space: pre-wrap")
dialog.open()
app.on_exception(handle_exception) when i stupidly write def save_the_thing():
btn_save.set_disabled(True) # should be btn_save.set_enabled(False)
... Yes, it's slightly hacky, but it seems pretty unlikely to cause any actual problems. I suppose the behavior could even be togglable at the application level... somehow. |
Beta Was this translation helpful? Give feedback.
The problem with
app.on_exception
is that the exception handler has no clue who raised the exception. Usually NiceGUI looks into its "slot stack" to see the most recent context. This includes the client (i.e. the browser tab), the UI element, and the slot. The slot is a concept from the Vue framework: Each element has a default slot, which is where child elements are placed. But some elements have multiple slots, e.g. QTable has one for the header, one for the pagination, one for body cells and so on. Therefore NiceGUI elements also have one or more slots.Apart from the technical details, it boils down to this: An app exception can happen anywhere in the code. It could also occur in a RE…