ALL software version info
Python : 3.13.13
holoviews : 1.22.1
bokeh : 3.9.0
numpy : 2.4.4
panel : 1.9.0
param : 2.4.0
Description of expected behavior and the observed behavior
holoviews.streams.Stream.trigger (in holoviews/streams.py, around line 213) iterates over subscribers in a bare for loop with no try/except:
with triggering_streams(streams):
for subscriber in subscribers:
subscriber(**dict(union))
If any single subscriber(...) call raises, the loop terminates and every subsequent subscriber in the list is silently skipped. There is no logging — the exception bubbles up through the asyncio task and is consumed by IOLoop._discard_future_result, leaving the visible state of the application a strict subset of what trigger() should have updated.
In practice this means a single buggy operation on one plot can silently freeze every other plot whose RangeX (or any shared stream) was supposed to refresh in the same trigger pass. The frozen plots show no visible error: their CDSes simply don't update, leaving them stuck on whatever state they were rendered in last.
I hit this in a multi-panel time-series setup driven by downsample1d. The triggering bug was an empty-Curve crash in interpolate_curve.pts_to_poststep (filed separately: ). But the visible symptom — sibling plots stuck on a zoomed-in sample even after wheel-out/Reset — took hours to diagnose because the actual exception fires once in the Python kernel for an unrelated operation, with no surfacing of "and by the way, 6 other plots' refreshes never ran."
Expected: a failing subscriber in Stream.trigger should not prevent other subscribers from running. At minimum the exception should be logged (or surfaced via the document-error machinery, the way Callback.on_msg does for CallbackError) so downstream debugging has a chance.
Observed: the first failing subscriber kills the loop. Subsequent subscribers are silently skipped. The exception escapes to the asyncio handler and is logged as a generic "Exception in callback" with no holoviews-side context.
Complete, minimal, self-contained example code that reproduces the issue
import holoviews as hv
import panel as pn
from holoviews.streams import Stream
hv.extension("bokeh")
# Set up two subscribers on a shared stream. The first raises; the
# second should still run, but doesn't.
stream = Stream.define("S", value=0)()
calls = []
def bad_subscriber(**kw):
calls.append("bad")
raise RuntimeError("simulated upstream bug")
def good_subscriber(**kw):
calls.append("good")
stream.add_subscriber(bad_subscriber)
stream.add_subscriber(good_subscriber)
try:
Stream.trigger([stream])
except Exception:
pass
print(f"calls: {calls}")
# Observed: ['bad'] — good_subscriber never ran.
# Expected: ['bad', 'good'] — good_subscriber should run despite bad's failure.
Suggested fix
Wrap each subscriber call in try/except inside Stream.trigger. Log (and optionally surface via state._handles-style document error display) failed subscribers, but always continue iterating. Something like:
import logging
log = logging.getLogger(__name__)
with triggering_streams(streams):
for subscriber in subscribers:
try:
subscriber(**dict(union))
except Exception:
log.exception(
"Stream subscriber %r raised; continuing remaining subscribers",
subscriber,
)
This matches the resilience already present in Callback.on_msg (which has dedicated CallbackError handling) and prevents one bad operation from silently affecting unrelated plots in the same trigger pass.
ALL software version info
Description of expected behavior and the observed behavior
holoviews.streams.Stream.trigger(inholoviews/streams.py, around line 213) iterates over subscribers in a bareforloop with no try/except:If any single
subscriber(...)call raises, the loop terminates and every subsequent subscriber in the list is silently skipped. There is no logging — the exception bubbles up through the asyncio task and is consumed byIOLoop._discard_future_result, leaving the visible state of the application a strict subset of whattrigger()should have updated.In practice this means a single buggy operation on one plot can silently freeze every other plot whose RangeX (or any shared stream) was supposed to refresh in the same trigger pass. The frozen plots show no visible error: their CDSes simply don't update, leaving them stuck on whatever state they were rendered in last.
I hit this in a multi-panel time-series setup driven by
downsample1d. The triggering bug was an empty-Curve crash ininterpolate_curve.pts_to_poststep(filed separately: ). But the visible symptom — sibling plots stuck on a zoomed-in sample even after wheel-out/Reset — took hours to diagnose because the actual exception fires once in the Python kernel for an unrelated operation, with no surfacing of "and by the way, 6 other plots' refreshes never ran."Expected: a failing subscriber in
Stream.triggershould not prevent other subscribers from running. At minimum the exception should be logged (or surfaced via the document-error machinery, the wayCallback.on_msgdoes forCallbackError) so downstream debugging has a chance.Observed: the first failing subscriber kills the loop. Subsequent subscribers are silently skipped. The exception escapes to the asyncio handler and is logged as a generic "Exception in callback" with no holoviews-side context.
Complete, minimal, self-contained example code that reproduces the issue
Suggested fix
Wrap each subscriber call in try/except inside
Stream.trigger. Log (and optionally surface viastate._handles-style document error display) failed subscribers, but always continue iterating. Something like:This matches the resilience already present in
Callback.on_msg(which has dedicatedCallbackErrorhandling) and prevents one bad operation from silently affecting unrelated plots in the same trigger pass.