Good day, All,
I wanted to pass along something that I’ve learned recently.
It is how we can utilize custom error handling of Dash callbacks and layouts once a server is deployed, and or you cant watch the console all the time. Also when running a flask server instead of running the app, even with debug=True
, you lose the error messages.
Anyways, to utilize this, we are going to add decorators to all the callbacks and layout functions.
TLDR:
import dash
from dash._callback import GLOBAL_CALLBACK_MAP
from dash._utils import to_json
import traceback
import utils.appFunction as apF
from dash import ctx
from dash.exceptions import PreventUpdate
def layout_decorator(error_message):
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except:
alertError(f"{(current_user or '')} - {error_message}", traceback.format_exc())
error = 'there was an issue, IT has been notified'
return html.Div([error,
apF.notification('error', error)])
return wrapper
return decorator
def callback_decorator(error_message, output):
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
if str(e) == '':
raise PreventUpdate
alertError(f"{(current_user or '')} - {error_message}",
f"""{output}
{traceback.format_exc()}""")
resp = {"app_notification": {
"children":
json.loads(to_json(apF.notification('error',
'there was an issue, IT has been notified')))
},
"loading": {'style': {'display': 'none'}}}
try:
if isinstance(output, list):
for o in output:
if 'disabled' in str(o):
resp[str(o).split('.')[0]] = {'disabled': False}
if 'notification' in str(o).lower():
resp[str(o).split('.')[0]] = {"children":
json.loads(to_json(apF.notification('error',
'there was an issue, IT has been notified',
ctx)))}
del resp['app_notification']
except:
if 'disabled' in str(output):
resp[str(output).split('.')[0]] = {'disabled': False}
if 'notification' in str(output).lower():
resp[str(output).split('.')[0]] = {"children":
json.loads(to_json(apF.notification('error',
'there was an issue, IT has been notified',
ctx)))}
del resp['app_notification']
pass
return json.dumps({
"multi": True,
"response": resp
})
return wrapper
return decorator
for pg in dash.page_registry.values():
if callable(pg['layout']):
pg['layout'] = layout_decorator(pg['path'] or pg['path_template'])(pg['layout'])
for k, v in GLOBAL_CALLBACK_MAP.items():
if v.get('callback'):
v['callback'] = callback_decorator('error in callback', v['output'])(v['callback'])
Now lets break is down.
def layout_decorator(error_message):
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except:
alertError(f"{(current_user or '')} - {error_message}", traceback.format_exc())
error = 'there was an issue, IT has been notified'
return html.Div([error,
apF.notification('error', error)])
return wrapper
return decorator
This assumes you are using functions for your layouts. It tries to perform the function, and upon error it will alertError
, in my setup this is configured to send me an email upon an error. The return statement is configured in this example to send the app a notification.
Now, lets take a look at the callback handler and how it differs from the layout handler.
def callback_decorator(error_message, output):
def decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except Exception as e:
if str(e) == '':
raise PreventUpdate
alertError(f"{(current_user or '')} - {error_message}",
f"""{output}
{traceback.format_exc()}""")
resp = {"app_notification": {
"children":
json.loads(to_json(apF.notification('error',
'there was an issue, IT has been notified')))
},
"loading": {'style': {'display': 'none'}}}
try:
if isinstance(output, list):
for o in output:
if 'disabled' in str(o):
resp[str(o).split('.')[0]] = {'disabled': False}
if 'notification' in str(o).lower():
resp[str(o).split('.')[0]] = {"children":
json.loads(to_json(apF.notification('error',
'there was an issue, IT has been notified',
ctx)))}
del resp['app_notification']
except:
if 'disabled' in str(output):
resp[str(output).split('.')[0]] = {'disabled': False}
if 'notification' in str(output).lower():
resp[str(output).split('.')[0]] = {"children":
json.loads(to_json(apF.notification('error',
'there was an issue, IT has been notified',
ctx)))}
del resp['app_notification']
pass
return json.dumps({
"multi": True,
"response": resp
})
return wrapper
return decorator
Notice, this is now passing the output from the callback, this is to help flip through the outputs for resetting things like if you are disabling buttons. This also checks to see if there are any notifications already displaying, I have a standard of my notification container to have notification
somewhere in the name. This also is configured to update the style of my loading screen in the event that I have it showing.
As an important note:
except Exception as e:
if str(e) == '':
raise PreventUpdate
^ the raise PreventUpdate
will flag this handler, thus checking to make sure there is an actual error string is import. If blank I just pass it along the chain.
Now the reason I utilize traceback
vs the generic error string, I like to know the line and what exactly happened. Makes it a lot easier to troubleshoot imo.
There are a couple of internal functions from dash
that we are pulling for this.
GLOBAL_CALLBACK_MAP
is obviously the map of all the callbacks in the app.
to_json
is helpful when converting dash components to their json response for the frontend to handle, I then parse this back to a dictionary for use in the children.
Now, I will say this will bug you if you have silently failing code, or the errors are passing too quickly in your console. So, be prepared.
There might be other ways to implement this which would be smoother, but this is working for me.
As a note, if you handle the found errors in the callbacks itself, then you wont trigger this handler.
I also prefer to handle the sweeping error handler because I do not want them app to crash, and this will notify you of everything.
Here is an example of the response: