[PR #40] [MERGED] Improve stacktraces clarity #68

Closed
opened 2026-02-16 02:16:28 -05:00 by yindo · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/run-llama/workflows-py/pull/40
Author: @masci
Created: 7/30/2025
Status: Merged
Merged: 8/7/2025
Merged by: @masci

Base: mainHead: massi/better-stacktraces


📝 Commits (3)

  • fb61802 fix: improve stacktraces clarity
  • 57c5fae fix unit tests
  • 6ffadc9 do the same if a retry policy is set

📊 Changes

5 files changed (+8 additions, -20 deletions)

View changed files

📝 src/workflows/context/context.py (+2 -7)
📝 src/workflows/events.py (+2 -3)
📝 tests/server/test_server_endpoints.py (+1 -1)
📝 tests/test_streaming.py (+1 -3)
📝 tests/test_workflow.py (+2 -6)

📄 Description

Sister PR: https://github.com/run-llama/llama_index/pull/19566

This PR removes an additional wrap of exceptions raised by a workflow run into an outer WorkflowRuntimeError. Wrapping the error has good intentions but it adds verbosity hiding the root cause of the error. Without the wrapper, users can take the last line of the stacktrace and navigate directly to the piece of code in their workflow that caused an error.

Not strictly related, I also removed an unneeded raise from the Event base class. I think for this library in particular we should prefer LBYL over EAFP in order to get shorter stacktraces that go to the point.

Example

Provided this workflow:

class MyWorkflow(Workflow):
    @step
    async def my_step(self, ev: StartEvent) -> StopEvent:
        return StopEvent(result=f"Hello, {ev.nope}!")


w = MyWorkflow()
result = await w.run(msg="World!")

Before

Exception in callback Dispatcher.span.<locals>.wrapper.<locals>.handle_future_result()() at /Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py:282
handle: <Handle Dispatcher.span.<locals>.wrapper.<locals>.handle_future_result()() at /Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py:282>
Traceback (most recent call last):
  File "/Users/massi/dev/workflows-py/src/workflows/events.py", line 59, in __getattr__
    return self._data[__name]
           ~~~~~~~~~~^^^^^^^^
KeyError: 'nope'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/massi/dev/workflows-py/src/workflows/context/context.py", line 696, in _step_worker
    new_ev = await instrumented_step(**kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py", line 375, in async_wrapper
    result = await func(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/massi/dev/workflows-py/main.py", line 11, in my_step
    return StopEvent(result=f"Hello, {ev.nope}!")
                                      ^^^^^^^
  File "/Users/massi/dev/workflows-py/src/workflows/events.py", line 61, in __getattr__
    raise AttributeError(
        f"'{self.__class__.__name__}' object has no attribute '{__name}'"
    )
AttributeError: 'StartEvent' object has no attribute 'nope'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/events.py", line 89, in _run
    self._context.run(self._callback, *self._args)
    ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py", line 295, in handle_future_result
    raise exception
  File "/Users/massi/dev/workflows-py/src/workflows/workflow.py", line 416, in _run_workflow
    raise exception_raised
  File "/Users/massi/dev/workflows-py/src/workflows/context/context.py", line 705, in _step_worker
    raise WorkflowRuntimeError(
        f"Error in step '{name}': {e!s}"
    ) from e
workflows.errors.WorkflowRuntimeError: Error in step 'my_step': 'StartEvent' object has no attribute 'nope'
Traceback (most recent call last):
  File "/Users/massi/dev/workflows-py/src/workflows/events.py", line 59, in __getattr__
    return self._data[__name]
           ~~~~~~~~~~^^^^^^^^
KeyError: 'nope'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/Users/massi/dev/workflows-py/src/workflows/context/context.py", line 696, in _step_worker
    new_ev = await instrumented_step(**kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py", line 375, in async_wrapper
    result = await func(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/massi/dev/workflows-py/main.py", line 11, in my_step
    return StopEvent(result=f"Hello, {ev.nope}!")
                                      ^^^^^^^
  File "/Users/massi/dev/workflows-py/src/workflows/events.py", line 61, in __getattr__
    raise AttributeError(
        f"'{self.__class__.__name__}' object has no attribute '{__name}'"
    )
AttributeError: 'StartEvent' object has no attribute 'nope'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/Users/massi/dev/workflows-py/main.py", line 20, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "/Users/massi/dev/workflows-py/main.py", line 16, in main
    result = await w.run(foo="bar")
             ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/massi/dev/workflows-py/src/workflows/workflow.py", line 416, in _run_workflow
    raise exception_raised
  File "/Users/massi/dev/workflows-py/src/workflows/context/context.py", line 705, in _step_worker
    raise WorkflowRuntimeError(
        f"Error in step '{name}': {e!s}"
    ) from e
workflows.errors.WorkflowRuntimeError: Error in step 'my_step': 'StartEvent' object has no attribute 'nope'

After

Traceback (most recent call last):
  File "/Users/massi/dev/workflows-py/main.py", line 20, in <module>
    asyncio.run(main())
    ~~~~~~~~~~~^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "/Users/massi/dev/workflows-py/main.py", line 16, in main
    result = await w.run(foo="bar")
             ^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/massi/dev/workflows-py/src/workflows/workflow.py", line 416, in _run_workflow
    raise exception_raised
  File "/Users/massi/dev/workflows-py/src/workflows/context/context.py", line 696, in _step_worker
    new_ev = await instrumented_step(**kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py", line 368, in async_wrapper
    result = await func(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/massi/dev/workflows-py/main.py", line 11, in my_step
    return StopEvent(result=f"Hello, {ev.nope}!")
                                      ^^^^^^^
  File "/Users/massi/dev/workflows-py/src/workflows/events.py", line 59, in __getattr__
    raise AttributeError(
        f"'{self.__class__.__name__}' object has no attribute '{__name}'"
    )
AttributeError: 'StartEvent' object has no attribute 'nope'

(note this requires the sister PR to be merged)


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/run-llama/workflows-py/pull/40 **Author:** [@masci](https://github.com/masci) **Created:** 7/30/2025 **Status:** ✅ Merged **Merged:** 8/7/2025 **Merged by:** [@masci](https://github.com/masci) **Base:** `main` ← **Head:** `massi/better-stacktraces` --- ### 📝 Commits (3) - [`fb61802`](https://github.com/run-llama/workflows-py/commit/fb61802eccddd632ba9a4540f5471c6c27a9e439) fix: improve stacktraces clarity - [`57c5fae`](https://github.com/run-llama/workflows-py/commit/57c5faedb54276c9b5784b9a2bbc388da1231b8a) fix unit tests - [`6ffadc9`](https://github.com/run-llama/workflows-py/commit/6ffadc9cd804ce1a7291c66a6aabee34deb746c6) do the same if a retry policy is set ### 📊 Changes **5 files changed** (+8 additions, -20 deletions) <details> <summary>View changed files</summary> 📝 `src/workflows/context/context.py` (+2 -7) 📝 `src/workflows/events.py` (+2 -3) 📝 `tests/server/test_server_endpoints.py` (+1 -1) 📝 `tests/test_streaming.py` (+1 -3) 📝 `tests/test_workflow.py` (+2 -6) </details> ### 📄 Description Sister PR: https://github.com/run-llama/llama_index/pull/19566 This PR removes an additional wrap of exceptions raised by a workflow run into an outer `WorkflowRuntimeError`. Wrapping the error has good intentions but it adds verbosity hiding the root cause of the error. Without the wrapper, users can take the last line of the stacktrace and navigate directly to the piece of code in their workflow that caused an error. Not strictly related, I also removed an unneeded `raise` from the `Event` base class. I think for this library in particular we should prefer LBYL over EAFP in order to get shorter stacktraces that go to the point. ## Example Provided this workflow: ```py class MyWorkflow(Workflow): @step async def my_step(self, ev: StartEvent) -> StopEvent: return StopEvent(result=f"Hello, {ev.nope}!") w = MyWorkflow() result = await w.run(msg="World!") ``` ## Before ``` Exception in callback Dispatcher.span.<locals>.wrapper.<locals>.handle_future_result()() at /Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py:282 handle: <Handle Dispatcher.span.<locals>.wrapper.<locals>.handle_future_result()() at /Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py:282> Traceback (most recent call last): File "/Users/massi/dev/workflows-py/src/workflows/events.py", line 59, in __getattr__ return self._data[__name] ~~~~~~~~~~^^^^^^^^ KeyError: 'nope' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/Users/massi/dev/workflows-py/src/workflows/context/context.py", line 696, in _step_worker new_ev = await instrumented_step(**kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py", line 375, in async_wrapper result = await func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/massi/dev/workflows-py/main.py", line 11, in my_step return StopEvent(result=f"Hello, {ev.nope}!") ^^^^^^^ File "/Users/massi/dev/workflows-py/src/workflows/events.py", line 61, in __getattr__ raise AttributeError( f"'{self.__class__.__name__}' object has no attribute '{__name}'" ) AttributeError: 'StartEvent' object has no attribute 'nope' The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/events.py", line 89, in _run self._context.run(self._callback, *self._args) ~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py", line 295, in handle_future_result raise exception File "/Users/massi/dev/workflows-py/src/workflows/workflow.py", line 416, in _run_workflow raise exception_raised File "/Users/massi/dev/workflows-py/src/workflows/context/context.py", line 705, in _step_worker raise WorkflowRuntimeError( f"Error in step '{name}': {e!s}" ) from e workflows.errors.WorkflowRuntimeError: Error in step 'my_step': 'StartEvent' object has no attribute 'nope' Traceback (most recent call last): File "/Users/massi/dev/workflows-py/src/workflows/events.py", line 59, in __getattr__ return self._data[__name] ~~~~~~~~~~^^^^^^^^ KeyError: 'nope' During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/Users/massi/dev/workflows-py/src/workflows/context/context.py", line 696, in _step_worker new_ev = await instrumented_step(**kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py", line 375, in async_wrapper result = await func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/massi/dev/workflows-py/main.py", line 11, in my_step return StopEvent(result=f"Hello, {ev.nope}!") ^^^^^^^ File "/Users/massi/dev/workflows-py/src/workflows/events.py", line 61, in __getattr__ raise AttributeError( f"'{self.__class__.__name__}' object has no attribute '{__name}'" ) AttributeError: 'StartEvent' object has no attribute 'nope' The above exception was the direct cause of the following exception: Traceback (most recent call last): File "/Users/massi/dev/workflows-py/main.py", line 20, in <module> asyncio.run(main()) ~~~~~~~~~~~^^^^^^^^ File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 195, in run return runner.run(main) ~~~~~~~~~~^^^^^^ File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 118, in run return self._loop.run_until_complete(task) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^ File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py", line 725, in run_until_complete return future.result() ~~~~~~~~~~~~~^^ File "/Users/massi/dev/workflows-py/main.py", line 16, in main result = await w.run(foo="bar") ^^^^^^^^^^^^^^^^^^^^^^ File "/Users/massi/dev/workflows-py/src/workflows/workflow.py", line 416, in _run_workflow raise exception_raised File "/Users/massi/dev/workflows-py/src/workflows/context/context.py", line 705, in _step_worker raise WorkflowRuntimeError( f"Error in step '{name}': {e!s}" ) from e workflows.errors.WorkflowRuntimeError: Error in step 'my_step': 'StartEvent' object has no attribute 'nope' ``` ## After ``` Traceback (most recent call last): File "/Users/massi/dev/workflows-py/main.py", line 20, in <module> asyncio.run(main()) ~~~~~~~~~~~^^^^^^^^ File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 195, in run return runner.run(main) ~~~~~~~~~~^^^^^^ File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/runners.py", line 118, in run return self._loop.run_until_complete(task) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^ File "/opt/homebrew/Cellar/python@3.13/3.13.5/Frameworks/Python.framework/Versions/3.13/lib/python3.13/asyncio/base_events.py", line 725, in run_until_complete return future.result() ~~~~~~~~~~~~~^^ File "/Users/massi/dev/workflows-py/main.py", line 16, in main result = await w.run(foo="bar") ^^^^^^^^^^^^^^^^^^^^^^ File "/Users/massi/dev/workflows-py/src/workflows/workflow.py", line 416, in _run_workflow raise exception_raised File "/Users/massi/dev/workflows-py/src/workflows/context/context.py", line 696, in _step_worker new_ev = await instrumented_step(**kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/massi/dev/llama_index/llama-index-instrumentation/src/llama_index_instrumentation/dispatcher.py", line 368, in async_wrapper result = await func(*args, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/massi/dev/workflows-py/main.py", line 11, in my_step return StopEvent(result=f"Hello, {ev.nope}!") ^^^^^^^ File "/Users/massi/dev/workflows-py/src/workflows/events.py", line 59, in __getattr__ raise AttributeError( f"'{self.__class__.__name__}' object has no attribute '{__name}'" ) AttributeError: 'StartEvent' object has no attribute 'nope' ``` (note this requires the sister PR to be merged) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
yindo added the pull-request label 2026-02-16 02:16:28 -05:00
yindo closed this issue 2026-02-16 02:16:28 -05:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: run-llama/workflows-py#68