#1452·loguru

TypeError in _better_exceptions on Python 3.14: tb_lineno is None for async CancelledError frames

Author: RRustomCreated Mar 25, 2026Updated Sep 4, 2026
Labelsduplicate

Description

On Python 3.14, loguru's _better_exceptions.py crashes when formatting tracebacks from asyncio.CancelledError raised in async frames. The traceback object has tb_lineno = None for certain frames, which causes linecache.getline() to fail.

Error

--- Logging error in loguru handler ---
Traceback (most recent call last):
  File ".venv/lib/python3.14/site-packages/loguru/_handler.py", line 147, in emit
    formatter_record["exception"] = "".join(lines)
  File ".venv/lib/python3.14/site-packages/loguru/_better_exceptions.py", line 573, in format_exception
    yield from self._format_exception(value, tb, ...)
  File ".venv/lib/python3.14/site-packages/loguru/_better_exceptions.py", line 454, in _format_exception
    frames, final_source = self._extract_frames(...)
  File ".venv/lib/python3.14/site-packages/loguru/_better_exceptions.py", line 239, in _extract_frames
    infos.append((get_info(tb.tb_frame, tb.tb_lineno), tb.tb_frame))
  File ".venv/lib/python3.14/site-packages/loguru/_better_exceptions.py", line 211, in get_info
    source = linecache.getline(filename, lineno).strip()
  File "/usr/local/lib/python3.14/linecache.py", line 27, in getline
    if 1 <= lineno <= len(lines):
TypeError: '<=' not supported between instances of 'int' and 'NoneType'

Root cause

In Python 3.14, tb.tb_lineno can be None for certain async frames — specifically CancelledError frames originating from anyio._core._sockets.connect_tcp. The get_info() function at line 211 of _better_exceptions.py passes lineno directly to linecache.getline() without a None check:

python
source = linecache.getline(filename, lineno).strip()

And CPython 3.14's linecache.getline() does:

python
if 1 <= lineno <= len(lines):  # TypeError when lineno is None

Reproduction

  • Python: 3.14.3
  • loguru: 0.7.3
  • Framework: FastAPI + uvicorn + anyio
  • Trigger: Any logger.exception() call that logs an asyncio.CancelledError originating from an async TCP connection frame

This happens with both backtrace=True and backtrace=False (the _extract_frames codepath runs in both cases). Setting backtrace=False works as a workaround only because the stdlib traceback formatter handles None lineno gracefully.

Suggested fix

Guard against None lineno in get_info():

python
def get_info(frame, lineno):
    if lineno is None:
        return ...  # return a default/empty source info
    source = linecache.getline(filename, lineno).strip()
    ...

Or in _extract_frames():

python
if tb.tb_lineno is not None:
    infos.append((get_info(tb.tb_frame, tb.tb_lineno), tb.tb_frame))

Workaround

Set backtrace=False on the loguru handler to bypass _better_exceptions formatting entirely:

python
logger.add(sys.stderr, backtrace=False, diagnose=False)

Environment

  • OS: Linux (EKS/Docker)
  • Python: 3.14.3
  • loguru: 0.7.3
  • anyio: 4.12.1
  • uvicorn: latest