When an async background callback (background=True with an async def function) raises an exception or PreventUpdate, the worker fails with:
UnboundLocalError: cannot access local variable 'user_callback_output' where it is not associated with a value
This masks the original error, so the real cause of the failure is never surfaced to the on_error handlers or the client.
Affected code
Both background callback managers have the bug in their async_run() path:
dash/background_callback/managers/celery_manager.py
dash/background_callback/managers/diskcache_manager.py
In async_run(), user_callback_output is only assigned inside the try block. When the callback raises, control jumps to the except block (which sets errored = True), then reaches:
if asyncio.iscoroutine(user_callback_output):
user_callback_output = await user_callback_output
Since user_callback_output was never assigned, this raises UnboundLocalError. The synchronous run() path does not have this bug because it initialises user_callback_output = None before the try.
Steps to reproduce
from dash import Dash, Input, Output, html, callback
from dash.exceptions import PreventUpdate
# ... configure a CeleryManager or DiskcacheManager as background_callback_manager
@callback(
Output("out", "children"),
Input("btn", "n_clicks"),
background=True,
prevent_initial_call=True,
)
async def handler(_):
raise PreventUpdate # or: raise Exception("boom")
Clicking the button raises UnboundLocalError in the worker instead of cleanly preventing the update / reporting the error.
Expected behaviour
An async background callback that raises PreventUpdate or an exception should behave like the synchronous case: the no-update result or background_callback_error is reported, and the original error is not masked.
Environment
- dash:
4.2.0
- Python:
3.12
- Manager: Celery (and Diskcache — both affected)
When an async background callback (
background=Truewith anasync deffunction) raises an exception orPreventUpdate, the worker fails with:This masks the original error, so the real cause of the failure is never surfaced to the
on_errorhandlers or the client.Affected code
Both background callback managers have the bug in their
async_run()path:dash/background_callback/managers/celery_manager.pydash/background_callback/managers/diskcache_manager.pyIn
async_run(),user_callback_outputis only assigned inside thetryblock. When the callback raises, control jumps to theexceptblock (which setserrored = True), then reaches:Since
user_callback_outputwas never assigned, this raisesUnboundLocalError. The synchronousrun()path does not have this bug because it initialisesuser_callback_output = Nonebefore thetry.Steps to reproduce
Clicking the button raises
UnboundLocalErrorin the worker instead of cleanly preventing the update / reporting the error.Expected behaviour
An async background callback that raises
PreventUpdateor an exception should behave like the synchronous case: the no-update result orbackground_callback_erroris reported, and the original error is not masked.Environment
4.2.03.12