Skip to content

fix(statistics): handle exceptions without a traceback in ErrorTracker#2035

Merged
vdusek merged 1 commit into
apify:masterfrom
anxkhn:fix/error-tracker-empty-traceback
Jul 7, 2026
Merged

fix(statistics): handle exceptions without a traceback in ErrorTracker#2035
vdusek merged 1 commit into
apify:masterfrom
anxkhn:fix/error-tracker-empty-traceback

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

ErrorTracker._get_file_and_line calls traceback.extract_tb(error.__traceback__)
and then unconditionally reads the last frame with error_traceback[-1]. For an
exception whose __traceback__ is None (for example a freshly constructed
exception that was never raised), extract_tb returns an empty list, so [-1]
raises IndexError.

_get_file_and_line is called from ErrorTracker.add, which is a best-effort
statistics path, so a valid Exception argument that happens to carry no
traceback crashes the tracker instead of being counted.

This guards the empty case: when there are no traceback frames, return an empty
file-and-line string, which is exactly the value the method already returns when
show_file_and_line_number is disabled. That empty value is already handled
everywhere downstream (it is used as the top-level grouping key in add, and
_get_error_repr renders an empty file-and-line as no prefix), so grouping and
the error representation stay correct.

  • Guard the empty-traceback case in ErrorTracker._get_file_and_line and return ''.
  • Add a regression test that adds a never-raised exception and asserts it is tracked without raising.

Issues

Testing

  • Added test_error_tracker_error_without_traceback in
    tests/unit/_statistics/test_error_tracker.py. It constructs ValueError('Some value error')
    (never raised, so __traceback__ is None), calls await error_tracker.add(...),
    and asserts total == 1, unique_error_count == 1, and that
    get_most_common_errors() reports 'ValueError:Some value error' (name and
    message, with no file:line prefix). The test fails with IndexError on
    master and passes with the fix.
  • uv run pytest tests/unit/_statistics/test_error_tracker.py -> 14 passed (offline).
  • uv run poe lint -> all checks passed.
  • uv run poe type-check on the changed files -> passed. (The full type-check reports
    pre-existing diagnostics only in the unrelated _pydantic_ai module, which needs an
    optional dependency; the changed files are clean.)

Checklist

  • CI passed

The diff (2 files, +16)

diff --git a/src/crawlee/statistics/_error_tracker.py b/src/crawlee/statistics/_error_tracker.py
index 20f827a..fd4a938 100644
--- a/src/crawlee/statistics/_error_tracker.py
+++ b/src/crawlee/statistics/_error_tracker.py
@@ -107,6 +107,9 @@ class ErrorTracker:
     def _get_file_and_line(self, error: Exception) -> str:
         if self.show_file_and_line_number:
             error_traceback = traceback.extract_tb(error.__traceback__)
+            if not error_traceback:
+                # The exception has no traceback (e.g. it was never raised).
+                return ''
             # Show only the most specific frame.
             return f'{error_traceback[-1].filename.split("/")[-1]}:{error_traceback[-1].lineno}'
         return ''
diff --git a/tests/unit/_statistics/test_error_tracker.py b/tests/unit/_statistics/test_error_tracker.py
index 26f02c5..d0dbac6 100644
--- a/tests/unit/_statistics/test_error_tracker.py
+++ b/tests/unit/_statistics/test_error_tracker.py
@@ -114,3 +114,16 @@ async def test_error_tracker_with_errors_chain() -> None:
             await error_tracker.add(e)
 
     assert error_tracker.get_most_common_errors()[0][0] == 'Zero division error'
+
+
+async def test_error_tracker_error_without_traceback() -> None:
+    """Test that an exception with no traceback (never raised) is tracked without crashing."""
+    error_tracker = ErrorTracker()
+
+    # A freshly constructed exception has `__traceback__` set to None.
+    await error_tracker.add(ValueError('Some value error'))
+
+    assert error_tracker.total == 1
+    assert error_tracker.unique_error_count == 1
+    # With no traceback there is no file and line, so the group has just the name and message.
+    assert error_tracker.get_most_common_errors()[0][0] == 'ValueError:Some value error'

…out a traceback

`ErrorTracker._get_file_and_line` called `traceback.extract_tb(error.__traceback__)`
and unconditionally indexed the last frame. For an exception that was never raised
(its `__traceback__` is `None`), `extract_tb` returns an empty list, so `[-1]`
raised `IndexError` out of the best-effort `ErrorTracker.add` statistics path.

Guard the empty case and return an empty file-and-line string, matching the
behaviour when `show_file_and_line_number` is disabled. Add a regression test
that adds a freshly constructed exception and asserts it is tracked without
crashing.

Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com>
@vdusek vdusek changed the title fix(statistics): avoid IndexError in ErrorTracker for exceptions without a traceback fix(statistics): handle exceptions without a traceback in ErrorTracker Jul 7, 2026
@vdusek vdusek merged commit be04c17 into apify:master Jul 7, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants