Unify `onerror` handler used by test framework
diff --git a/src/postamble_minimal.js b/src/postamble_minimal.js
index 7fefb35..7a7fd1b 100644
--- a/src/postamble_minimal.js
+++ b/src/postamble_minimal.js
@@ -60,6 +60,7 @@
   // Export needed variables that worker.js needs to Module.
   Module['HEAPU32'] = HEAPU32;
   Module['__emscripten_thread_init'] = __emscripten_thread_init;
+  Module['__emscripten_thread_exit'] = __emscripten_thread_exit;
   Module['_pthread_self'] = _pthread_self;
 
   if (ENVIRONMENT_IS_PTHREAD) {
diff --git a/src/preamble.js b/src/preamble.js
index 4fba1b2..cc899d0 100644
--- a/src/preamble.js
+++ b/src/preamble.js
@@ -305,17 +305,6 @@
        'JS engine does not provide full typed array support');
 #endif
 
-#if IN_TEST_HARNESS
-// Test runs in browsers should always be free from uncaught exceptions. If an uncaught exception is thrown, we fail browser test execution in the REPORT_RESULT() macro to output an error value.
-if (ENVIRONMENT_IS_WEB) {
-  window.addEventListener('error', function(e) {
-    if (e.message.includes('unwind')) return;
-    console.error('Page threw an exception ' + e);
-    Module['pageThrewException'] = true;
-  });
-}
-#endif
-
 #if IMPORTED_MEMORY
 // In non-standalone/normal mode, we create the memory here.
 #include "runtime_init_memory.js"
diff --git a/tests/browser/async_bad_list.cpp b/tests/browser/async_bad_list.cpp
index c11d434..ae1a6b3 100644
--- a/tests/browser/async_bad_list.cpp
+++ b/tests/browser/async_bad_list.cpp
@@ -7,32 +7,10 @@
 #include <emscripten.h>
 
 int main() {
-  int x = EM_ASM_INT({
-    window.onerror = function(e) {
-      var message = e.toString();
-      var success = message.indexOf("unreachable") >= 0 || // firefox
-                    message.indexOf("Script error.") >= 0; // chrome
-      if (success && !Module.reported) {
-        Module.reported = true;
-        console.log("reporting success");
-        // manually REPORT_RESULT; we shouldn't call back into native code at this point
-        var xhr = new XMLHttpRequest();
-        xhr.open("GET", "http://localhost:8888/report_result?0");
-        xhr.onload = xhr.onerror = function() {
-          window.close();
-        };
-        xhr.send();
-      }
-    };
-    return 0;
-  });
-
   emscripten_sleep(1);
 
-  // We should not get here - the unwind will fail as we did now all the right
-  // functions - this function should be instrumented, but will not be.
+  // We should not get here - the unwind will fail as we did not list all the
+  // right functions - this function should be instrumented, but will not be.
   puts("We should not get here!");
-  REPORT_RESULT(1);
-
-  return 0;
+  return 1;
 }
diff --git a/tests/browser/async_returnvalue.cpp b/tests/browser/async_returnvalue.cpp
index c525af0..4d8589b 100644
--- a/tests/browser/async_returnvalue.cpp
+++ b/tests/browser/async_returnvalue.cpp
@@ -30,24 +30,6 @@
 #endif
 
 int main() {
-#ifdef BAD
-  EM_ASM({
-    window.onerror = function(e) {
-      var success = e.toString().indexOf("import sync_tunnel was not in ASYNCIFY_IMPORTS, but changed the state") > 0;
-      if (success && !Module.reported) {
-        Module.reported = true;
-        console.log("reporting success");
-        // manually REPORT_RESULT; we shouldn't call back into native code at this point
-        var xhr = new XMLHttpRequest();
-        xhr.open("GET", "http://localhost:8888/report_result?0");
-        xhr.onload = xhr.onerror = function() {
-          window.close();
-        };
-        xhr.send();
-      }
-    };
-  });
-#endif
   int x;
   x = sync_tunnel(0);
   assert(x == 1);
@@ -68,15 +50,13 @@
   y = sync_tunnel_bool(true);
   assert(y == false);
 
-
-
 #ifdef BAD
   // We should not get here.
   printf("We should not get here\n");
+  return 1;
 #else
   // Success!
   REPORT_RESULT(0);
-#endif
-
   return 0;
+#endif
 }
diff --git a/tests/browser_reporting.js b/tests/browser_reporting.js
index eb04d5b..6528e3e 100644
--- a/tests/browser_reporting.js
+++ b/tests/browser_reporting.js
@@ -1,4 +1,6 @@
 var hasModule = typeof Module === 'object' && Module;
+var hasWindow = typeof window === 'object' && window;
+var keepWindowAlive = false;
 
 /** @param {boolean=} sync
     @param {number=} port */
@@ -6,16 +8,15 @@
   port = port || 8888;
   if (reportResultToServer.reported) {
     // Only report one result per test, even if the test misbehaves and tries to report more.
-    reportErrorToServer("excessive reported results, sending " + result + ", test will fail");
+    reportStderrToServer("excessive reported results, sending " + result + ", test will fail");
   }
   reportResultToServer.reported = true;
   var xhr = new XMLHttpRequest();
-  if (hasModule && Module['pageThrewException']) {
-    result = 'pageThrewException';
-  }
   xhr.open('GET', 'http://localhost:' + port + '/report_result?' + result, !sync);
   xhr.send();
-  if (typeof window === 'object' && window && hasModule && !Module['pageThrewException'] /* for easy debugging, don't close window on failure */) setTimeout(function() { window.close() }, 1000);
+  if (hasWindow && hasModule && !keepWindowAlive) {
+    setTimeout(function() { window.close() }, 1000);
+  }
 }
 
 /** @param {boolean=} sync
@@ -25,18 +26,35 @@
   reportResultToServer(result, sync, port);
 }
 
-function reportErrorToServer(message) {
+function reportStderrToServer(message) {
   var xhr = new XMLHttpRequest();
   xhr.open('GET', encodeURI('http://localhost:8888?stderr=' + message));
   xhr.send();
 }
 
+function reportExceptionToServer(e) {
+  var xhr = new XMLHttpRequest();
+  xhr.open('GET', encodeURI('http://localhost:8888?exception=' + e.message + ' / ' + e.stack));
+  xhr.send();
+}
+
 if (typeof window === 'object' && window) {
   function report_error(e) {
+    var message = e.message || e;
+    if (e.error) {
+      message = e.error.message;
+    }
+    // MINIMAL_RUNTIME lets unwind exceptions remain uncaught, and these
+    // should not be considered actually errors.
+    if (message == 'unwind') {
+      return;
+    }
+    console.error("emtest: got top level error: " + message);
+    // We report aborts via the onAbort handler so can ignore them here
+    if (message.indexOf(' abort(') != -1)
+      return;
     // MINIMAL_RUNTIME doesn't handle exit or call the below onExit handler
     // so we detect the exit by parsing the uncaught exception message.
-    var message = e.message || e;
-    console.error("got top level error: " + message);
     var offset = message.indexOf('exit(');
     if (offset != -1) {
       var status = message.substring(offset + 5);
@@ -45,9 +63,14 @@
       console.error(status);
       maybeReportResultToServer('exit:' + status);
     } else {
-      var xhr = new XMLHttpRequest();
-      xhr.open('GET', encodeURI('http://localhost:8888?exception=' + e.message + ' / ' + e.stack));
-      xhr.send();
+      reportExceptionToServer(e);
+      /*
+       * Also report the exception as the result of the test if non has been
+       * reported yet
+       * For easy debugging, don't close window on failure.
+       */
+      keepWindowAlive = true;
+      maybeReportResultToServer('exception:' + e.message);
     }
   }
   window.addEventListener('error', report_error);
diff --git a/tests/common.py b/tests/common.py
index b60ba67..0b8dac5 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -1334,7 +1334,7 @@
   #                     synchronously, so we have a timeout, which can be hit if the VM
   #                     we run on stalls temporarily), so we let each test try more than
   #                     once by default
-  def run_browser(self, html_file, message, expectedResult=None, timeout=None, extra_tries=1):
+  def run_browser(self, html_file, message, expectedResult=None, timeout=None, extra_tries=1, assert_all=False):
     if not has_browser():
       return
     if BrowserCore.unresponsive_tests >= BrowserCore.MAX_UNRESPONSIVE_TESTS:
@@ -1372,12 +1372,16 @@
           # verify the result, and try again if we should do so
           output = unquote(output)
           try:
-            self.assertContained(expectedResult, output)
+            if assert_all:
+              for o in expectedResult:
+                self.assertContained(o, output)
+            else:
+              self.assertContained(expectedResult, output)
           except Exception as e:
             if extra_tries > 0:
               print('[test error (see below), automatically retrying]')
               print(e)
-              return self.run_browser(html_file, message, expectedResult, timeout, extra_tries - 1)
+              return self.run_browser(html_file, message, expectedResult, timeout, extra_tries - 1, assert_all=assert_all)
             else:
               raise e
       finally:
diff --git a/tests/emscripten_throw_number.c b/tests/emscripten_throw_number.c
index 6c6571c..b12f534 100644
--- a/tests/emscripten_throw_number.c
+++ b/tests/emscripten_throw_number.c
@@ -3,7 +3,5 @@
 int main()
 {
 	emscripten_throw_number(42);
-#ifdef REPORT_RESULT
-	REPORT_RESULT(1); // failed
-#endif
+	__builtin_trap();
 }
diff --git a/tests/emscripten_throw_number_pre.js b/tests/emscripten_throw_number_pre.js
deleted file mode 100644
index 215bd7d..0000000
--- a/tests/emscripten_throw_number_pre.js
+++ /dev/null
@@ -1,8 +0,0 @@
-addEventListener('error', function(event) {
-  event.preventDefault();
-  event.stopPropagation();
-  var result = event.error === 42 ? 0 : 1;
-  var xhr = new XMLHttpRequest();
-  xhr.open('GET', 'http://localhost:8888/report_result?' + result, true);
-  xhr.send();
-});
diff --git a/tests/emscripten_throw_string.c b/tests/emscripten_throw_string.c
index b09e8eb..c8b84bd 100644
--- a/tests/emscripten_throw_string.c
+++ b/tests/emscripten_throw_string.c
@@ -3,7 +3,5 @@
 int main()
 {
 	emscripten_throw_string("Hello!");
-#ifdef REPORT_RESULT
-	REPORT_RESULT(1); // failed
-#endif
+	__builtin_trap();
 }
diff --git a/tests/emscripten_throw_string_pre.js b/tests/emscripten_throw_string_pre.js
deleted file mode 100644
index 5a45846..0000000
--- a/tests/emscripten_throw_string_pre.js
+++ /dev/null
@@ -1,8 +0,0 @@
-addEventListener('error', function(event) {
-  event.preventDefault();
-  event.stopPropagation();
-  var result = event.error === 'Hello!' ? 0 : 1;
-  var xhr = new XMLHttpRequest();
-  xhr.open('GET', 'http://localhost:8888/report_result?' + result, true);
-  xhr.send();
-});
diff --git a/tests/test_browser.py b/tests/test_browser.py
index 52d85de..32df528 100644
--- a/tests/test_browser.py
+++ b/tests/test_browser.py
@@ -627,14 +627,6 @@
           <center><canvas id='canvas' width='256' height='256'></canvas></center>
           <hr><div id='output'></div><hr>
           <script type='text/javascript'>
-            window.onerror = function(error) {
-              window.onerror = null;
-              var result = error.indexOf("test.data") >= 0 ? 1 : 0;
-              var xhr = new XMLHttpRequest();
-              xhr.open('GET', 'http://localhost:8888/report_result?' + result, true);
-              xhr.send();
-              setTimeout(function() { window.close() }, 1000);
-            }
             var Module = {
               locateFile: function (path, prefix) {if (path.endsWith(".wasm")) {return prefix + path;} else {return "''' + assetLocalization + r'''" + path;}},
               print: (function() {
@@ -653,17 +645,17 @@
       setup("")
       self.compile_btest(['main.cpp', '--shell-file', 'on_window_error_shell.html', '--preload-file', 'data.txt', '-o', 'test.html'])
       shutil.move('test.data', 'missing.data')
-      self.run_browser('test.html', '', '/report_result?1')
+      self.run_browser('test.html', '', ['/report_result?exception:', 'test.data'], assert_all=True)
 
       # test unknown protocol should go through xhr.onerror
       setup("unknown_protocol://")
       self.compile_btest(['main.cpp', '--shell-file', 'on_window_error_shell.html', '--preload-file', 'data.txt', '-o', 'test.html'])
-      self.run_browser('test.html', '', '/report_result?1')
+      self.run_browser('test.html', '', ['/report_result?exception:', 'test.data'], assert_all=True)
 
       # test wrong protocol and port
       setup("https://localhost:8800/")
       self.compile_btest(['main.cpp', '--shell-file', 'on_window_error_shell.html', '--preload-file', 'data.txt', '-o', 'test.html'])
-      self.run_browser('test.html', '', '/report_result?1')
+      self.run_browser('test.html', '', ['/report_result?exception:', 'test.data'], assert_all=True)
 
     test()
 
@@ -3321,22 +3313,33 @@
   # ASYNCIFY_IMPORTS.
   # To make the test more precise we also use ASYNCIFY_IGNORE_INDIRECT here.
   @parameterized({
-    'normal': (['-s', 'ASYNCIFY_IMPORTS=[sync_tunnel, sync_tunnel_bool]'],), # noqa
-    'response': (['-s', 'ASYNCIFY_IMPORTS=@filey.txt'],), # noqa
-    'nothing': (['-DBAD'],), # noqa
-    'empty_list': (['-DBAD', '-s', 'ASYNCIFY_IMPORTS=[]'],), # noqa
-    'em_js_bad': (['-DBAD', '-DUSE_EM_JS'],), # noqa
+    'normal': (False, ['-s', 'ASYNCIFY_IMPORTS=[sync_tunnel, sync_tunnel_bool]'],), # noqa
+    'response': (False, ['-s', 'ASYNCIFY_IMPORTS=@filey.txt'],), # noqa
+    'nothing': (True, ['-DBAD'],), # noqa
+    'empty_list': (True, ['-DBAD', '-s', 'ASYNCIFY_IMPORTS=[]'],), # noqa
+    'em_js_bad': (True, ['-DBAD', '-DUSE_EM_JS'],), # noqa
   })
-  def test_async_returnvalue(self, args):
+  def test_async_returnvalue(self, bad, args):
+    if bad:
+      expected = [
+        'exception:Uncaught Error: import sync_tunnel was not in ASYNCIFY_IMPORTS, but changed the state', # firefox
+        'exception:Error: import sync_tunnel was not in ASYNCIFY_IMPORTS, but changed the state' # chrome
+      ]
+    else:
+      expected = "0"
     if '@' in str(args):
       create_file('filey.txt', 'sync_tunnel\nsync_tunnel_bool\n')
-    self.btest('browser/async_returnvalue.cpp', '0', args=['-s', 'ASYNCIFY', '-s', 'ASYNCIFY_IGNORE_INDIRECT', '--js-library', test_file('browser/async_returnvalue.js')] + args + ['-s', 'ASSERTIONS'])
+    self.btest('browser/async_returnvalue.cpp', expected, args=['-s', 'ASYNCIFY', '-s', 'ASYNCIFY_IGNORE_INDIRECT', '--js-library', test_file('browser/async_returnvalue.js')] + args + ['-s', 'ASSERTIONS'])
 
   def test_async_stack_overflow(self):
     self.btest('browser/async_stack_overflow.cpp', 'abort:RuntimeError: unreachable', args=['-s', 'ASYNCIFY', '-s', 'ASYNCIFY_STACK_SIZE=4'])
 
   def test_async_bad_list(self):
-    self.btest('browser/async_bad_list.cpp', '0', args=['-s', 'ASYNCIFY', '-s', 'ASYNCIFY_ONLY=[waka]', '--profiling'])
+    expected = [
+        'exception:RuntimeError: unreachable executed', # firefox
+        'exception:Uncaught RuntimeError: unreachable', # chrome
+    ]
+    self.btest('browser/async_bad_list.cpp', expected, args=['-s', 'ASYNCIFY', '-s', 'ASYNCIFY_ONLY=[waka]', '--profiling'])
 
   # Tests that when building with -s MINIMAL_RUNTIME=1, the build can use -s MODULARIZE=1 as well.
   def test_minimal_runtime_modularize(self):
@@ -4303,8 +4306,9 @@
     # Check an absolute js code size, with some slack.
     size = os.path.getsize('test.js')
     print('size:', size)
-    # Note that this size includes test harness additions (for reporting the result, etc.).
-    self.assertLess(abs(size - 5629), 100)
+    # Note that this size includes test harness additions (so will change when, for example,
+    # browser_reporting.js changed)
+    self.assertLess(abs(size - 5496), 100)
 
   # Tests that it is possible to initialize and render WebGL content in a pthread by using OffscreenCanvas.
   # -DTEST_CHAINED_WEBGL_CONTEXT_PASSING: Tests that it is possible to transfer WebGL canvas in a chain from main thread -> thread 1 -> thread 2 and then init and render WebGL content there.
@@ -4984,10 +4988,10 @@
     self.btest(test_file('emscripten_console_log.c'), '0', args=['--pre-js', test_file('emscripten_console_log_pre.js')])
 
   def test_emscripten_throw_number(self):
-    self.btest(test_file('emscripten_throw_number.c'), '0', args=['--pre-js', test_file('emscripten_throw_number_pre.js')])
+    self.btest(test_file('emscripten_throw_number.c'), 'exception:Uncaught 42')
 
   def test_emscripten_throw_string(self):
-    self.btest(test_file('emscripten_throw_string.c'), '0', args=['--pre-js', test_file('emscripten_throw_string_pre.js')])
+    self.btest(test_file('emscripten_throw_string.c'), 'exception:Uncaught Hello!')
 
   # Tests that Closure run in combination with -s ENVIRONMENT=web mode works with a minimal console.log() application
   def test_closure_in_web_only_target_environment_console_log(self):