asyncio.TaskGroup, added in Python 3.11 (October 2022), differs from gather in two ways that change error handling. When one child task raises, the group cancels all remaining children and waits for them. And the failure is delivered as an ExceptionGroup, so a plain except ValueError around the async with block does not catch a ValueError raised inside a task.
Use except* to handle grouped exceptions, as in except* ValueError as eg:, where eg.exceptions holds the matching ones. Code that must run on 3.10 can use the exceptiongroup backport, which also provides catch.
The cancellation behaviour is the point of the API, but it means a task that swallows asyncio.CancelledError prevents the group from finishing and the async with block hangs. Never catch bare Exception in a coroutine without re-raising cancellation; in 3.8 and later CancelledError inherits from BaseException specifically so that except Exception does not catch it, but except BaseException and bare except: still do.