Repository: pyodide/pyodide

Issue:
Aborted fetch requests freeze REPL
## 🐛 Bug

Fetch requests aborted (using the [signal](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal/timeout_static#examples) options) freeze REPL, and do not raise exception.

### To Reproduce

```python
def fetch_response(url, timeout):
    options = js.Object.new()
    options.signal = js.AbortSignal.timeout(timeout)
    return js.fetch(url, options)

response = await fetch_response('slow api', 1)
```


Dev Console shows:
```
Uncaught (in promise) PythonError: TypeError: invalid exception object

    at new_error (pyodide.asm.js:9:14992)
    at pyodide.asm.wasm:0x152d67
    at pyodide.asm.wasm:0x152e6c
    at Module.callPyObjectKwargs (pyodide.asm.js:9:75811)
    at Module.callPyObject (pyodide.asm.js:9:76020)
    at onRejected (pyodide.asm.js:9:59090)
```

This appears to occur because the `PyodideFuture` object, which the fetch returns, expects to receive a python Exception object on rejection.   Instead, the returned `PyodideFuture` object gets a `JsProxy` of an `AbortError` (`DOMException`).  A  `JsProxy` of a `DOMException` can't be raised in pyodide. 

```python
>>> import js
>>> raise js.DOMException.new('')
```
```
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: exceptions must derive from BaseException
```

### Expected behavior

Should raise a `JsException`. 

Possible solution: Allow js.DOMException objects to be raised much like js.Error objects:

```python
>>> from pyodide.webloop import PyodideFuture
>>> fut = PyodideFuture()
>>> fut.set_exception(js.Error('hi'))
>>> fut.exception()
Error: hi
```



### Environment

- Pyodide Version<0.23.2>:
```
>>> import pyodide
>>> pyodide.__version__
'0.23.2'
```
- Browser version<Chrome 113.0.5672.114->:



Discussion:
Interesting... This happens only in Chrome: in Firefox `raise js.DomException("")` works as expected. 
Thanks for the report @stephenmccalman!
This reproduces all the way back to Pyodide 0.17.0. It's a bit surprising nobody has noticed it before.
It seems that we feature detect exceptions with:
```
hasProperty(obj, "name") &&
hasProperty(obj, "message") &&
hasProperty(obj, "stack")
```
In Chrome, `DOMException` has no `stack` property (but in firefox it has it).
Interestingly this behavior of chrome is not compliant with a "should" in the whatwg spec:

> if an implementation gives native [Error](https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-error-objects) objects special powers or nonstandard properties (such as a stack property), it should also expose those on [DOMException](https://webidl.spec.whatwg.org/#idl-DOMException) objects.

https://webidl.spec.whatwg.org/#es-DOMException-specialness


This bug also occurs in Safari.

Resolving patch:
diff --git a/src/py/_pyodide/_core_docs.py b/src/py/_pyodide/_core_docs.py
--- a/src/py/_pyodide/_core_docs.py
+++ b/src/py/_pyodide/_core_docs.py
@@ -988,6 +988,10 @@ def _new_exc(cls, name: str, message: str = "", stack: str = "") -> "JsException
         result.stack = stack
         return result
 
+    @classmethod
+    def new(cls, *args: Any) -> "JsException":
+        return cls()
+
     def __str__(self):
         return f"{self.name}: {self.message}"
 


Test patch:
diff --git a/src/tests/test_pyodide.py b/src/tests/test_pyodide.py
--- a/src/tests/test_pyodide.py
+++ b/src/tests/test_pyodide.py
@@ -583,6 +583,18 @@ def test_run_python_js_error(selenium):
     )
 
 
+@pytest.mark.xfail_browsers(node="No DOMException in node")
+@run_in_pyodide
+def test_run_python_dom_error(selenium):
+    import pytest
+
+    from js import DOMException
+    from pyodide.ffi import JsException
+
+    with pytest.raises(JsException, match="oops"):
+        raise DOMException.new("oops")
+
+
 def test_run_python_locals(selenium):
     selenium.run_js(
         """
diff --git a/src/tests/test_typeconversions.py b/src/tests/test_typeconversions.py
--- a/src/tests/test_typeconversions.py
+++ b/src/tests/test_typeconversions.py
@@ -951,6 +951,10 @@ def test_dict_js2py2js(selenium):
 def test_error_js2py2js(selenium):
     selenium.run_js("self.err = new Error('hello there?');")
     assert_js_to_py_to_js(selenium, "err")
+    if selenium.browser == "node":
+        return
+    selenium.run_js("self.err = new DOMException('hello there?');")
+    assert_js_to_py_to_js(selenium, "err")
 
 
 def test_error_py2js2py(selenium):