TypeError in _better_exceptions on Python 3.14: tb_lineno is None for async CancelledError frames
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:
source = linecache.getline(filename, lineno).strip()And CPython 3.14's linecache.getline() does:
if 1 <= lineno <= len(lines): # TypeError when lineno is NoneReproduction
- Python: 3.14.3
- loguru: 0.7.3
- Framework: FastAPI + uvicorn + anyio
- Trigger: Any
logger.exception()call that logs anasyncio.CancelledErrororiginating 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():
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():
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:
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
Source: Delgan/loguru