Repository: jupyter/jupyter_core

Issue:
`run_sync` handles `RuntimeError` raised by coroutine incorrectly
## Problem

The function `jupyter_core.utils.run_sync` mistakenly catches a `RuntimeError` raised by a coroutine and assumes no event loop is running.

If a coroutine handled by `run_sync` raises a `RuntimeError` while an event loop is running, it assumes the `RuntimeError` comes from `asyncio.get_running_loop()`. This causes calling any coroutine that raises a `RuntimeError` to raise an error that no event loop is running, since `run_sync` will attempt to set a new event loop.

## How to reproduce

Open a jupyter notebook, or any other context where an event loop is already running. Execute this code:

```python
from jupyter_core.utils import run_sync
async def f():
    raise RuntimeError("My runtime error")

run_sync(f)()
```

## Output

The code raises a `RuntimeError: This event loop is already running`

<details><summary>Full traceback</summary>

```
---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[32], line 5
      2 async def f():
      3     raise RuntimeError
----> 5 run_sync(f)()

File [/opt/conda/lib/python3.9/site-packages/jupyter_core/utils/__init__.py:165](http://localhost:8888/opt/conda/lib/python3.9/site-packages/jupyter_core/utils/__init__.py#line=164), in run_sync.<locals>.wrapped(*args, **kwargs)
    163 # Run the loop for this thread.
    164 loop = ensure_event_loop()
--> 165 return loop.run_until_complete(inner)

File [/opt/conda/lib/python3.9/asyncio/base_events.py:623](http://localhost:8888/opt/conda/lib/python3.9/asyncio/base_events.py#line=622), in BaseEventLoop.run_until_complete(self, future)
    612 """Run until the Future is done.
    613 
    614 If the argument is a coroutine, it is wrapped in a Task.
   (...)
    620 Return the Future's result, or raise its exception.
    621 """
    622 self._check_closed()
--> 623 self._check_running()
    625 new_task = not futures.isfuture(future)
    626 future = tasks.ensure_future(future, loop=self)

File [/opt/conda/lib/python3.9/asyncio/base_events.py:583](http://localhost:8888/opt/conda/lib/python3.9/asyncio/base_events.py#line=582), in BaseEventLoop._check_running(self)
    581 def _check_running(self):
    582     if self.is_running():
--> 583         raise RuntimeError('This event loop is already running')
    584     if events._get_running_loop() is not None:
    585         raise RuntimeError(
    586             'Cannot run the event loop while another loop is running')

RuntimeError: This event loop is already running
```

</details>


## Expected output

The code should raise `RuntimeError: My runtime error`.

Discussion:

Solution patch:
diff --git a/jupyter_core/utils/__init__.py b/jupyter_core/utils/__init__.py
index 4420c1a..665eac2 100644
--- a/jupyter_core/utils/__init__.py
+++ b/jupyter_core/utils/__init__.py
@@ -151,18 +151,17 @@ def wrapped(*args: Any, **kwargs: Any) -> Any:
         name = threading.current_thread().name
         inner = coro(*args, **kwargs)
         try:
-            # If a loop is currently running in this thread,
-            # use a task runner.
             asyncio.get_running_loop()
-            if name not in _runner_map:
-                _runner_map[name] = _TaskRunner()
-            return _runner_map[name].run(inner)
         except RuntimeError:
-            pass
-
-        # Run the loop for this thread.
-        loop = ensure_event_loop()
-        return loop.run_until_complete(inner)
+            # No loop running, run the loop for this thread.
+            loop = ensure_event_loop()
+            return loop.run_until_complete(inner)
+
+        # Loop is currently running in this thread,
+        # use a task runner.
+        if name not in _runner_map:
+            _runner_map[name] = _TaskRunner()
+        return _runner_map[name].run(inner)
 
     wrapped.__doc__ = coro.__doc__
     return wrapped


Test patch:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index b3d485b..5fffd91 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -56,6 +56,24 @@ async def foo():
 
     asyncio.run(foo())
 
+    error_msg = "__foo__"
+
+    async def error():
+        raise RuntimeError(error_msg)
+
+    error_sync = run_sync(error)
+
+    def test_error_sync():
+        with pytest.raises(RuntimeError, match=error_msg):
+            error_sync()
+
+    test_error_sync()
+
+    async def with_running_loop():
+        test_error_sync()
+
+    asyncio.run(with_running_loop())
+
 
 def test_ensure_async():
     async def main():