@@ -359,6 +359,52 @@ async def idx(keys: list[int]) -> list[int]:
359359 assert value_d == 4
360360
361361
362+ @pytest .mark .asyncio
363+ async def test_cancelled_future_with_failing_loader ():
364+ """When the load function raises and some futures are already cancelled,
365+ the error handler should skip cancelled futures instead of raising
366+ InvalidStateError.
367+
368+ This reproduces a real-world scenario where a client disconnects
369+ mid-request: some DataLoader futures get cancelled by the event loop,
370+ then the batch load function fails (e.g. because the executor is torn
371+ down). The error handler must not crash trying to set_exception on
372+ the already-cancelled futures.
373+ """
374+
375+ async def failing_loader (keys : list [int ]) -> list [int ]:
376+ raise RuntimeError ("executor is broken" )
377+
378+ loader = DataLoader (load_fn = failing_loader , cache = False )
379+
380+ future_a = cast ("Future[Any]" , loader .load (1 ))
381+ future_b = cast ("Future[Any]" , loader .load (2 ))
382+ future_c = cast ("Future[Any]" , loader .load (3 ))
383+
384+ # Simulate a client disconnect cancelling one of the futures before
385+ # the batch dispatches
386+ future_a .cancel ()
387+
388+ # Let the event loop dispatch the batch — this must not raise
389+ # InvalidStateError from trying to set_exception on the cancelled future.
390+ # Two yields: one for call_soon to fire create_task, one for the task to run.
391+ await asyncio .sleep (0 )
392+ await asyncio .sleep (0 )
393+
394+ # The cancelled future should remain cleanly cancelled — not broken by
395+ # an InvalidStateError inside dispatch_batch
396+ assert future_a .cancelled ()
397+ with pytest .raises (asyncio .CancelledError ):
398+ future_a .result ()
399+
400+ # The non-cancelled futures should have received the loader's exception
401+ # (without the fix, dispatch_batch crashes on future_a before reaching these)
402+ with pytest .raises (RuntimeError , match = "executor is broken" ):
403+ future_b .result ()
404+ with pytest .raises (RuntimeError , match = "executor is broken" ):
405+ future_c .result ()
406+
407+
362408@pytest .mark .asyncio
363409async def test_cache_override ():
364410 class TestCache (AbstractCache [int , int ]):
0 commit comments