diff --git a/src/mono/browser/browser.proj b/src/mono/browser/browser.proj
index 38d6f035e6cf36..a2ac05d51b730d 100644
--- a/src/mono/browser/browser.proj
+++ b/src/mono/browser/browser.proj
@@ -206,6 +206,7 @@
       
       
       
+      
     
     
     
diff --git a/src/mono/browser/runtime/cwraps.ts b/src/mono/browser/runtime/cwraps.ts
index 3ce3302e3b3514..ed64d5c541d4b8 100644
--- a/src/mono/browser/runtime/cwraps.ts
+++ b/src/mono/browser/runtime/cwraps.ts
@@ -63,7 +63,6 @@ const fn_signatures: SigLine[] = [
     [true, "mono_wasm_intern_string_ref", "void", ["number"]],
 
     [false, "mono_wasm_exit", "void", ["number"]],
-    [false, "mono_wasm_abort", "void", []],
     [true, "mono_wasm_getenv", "number", ["string"]],
     [true, "mono_wasm_set_main_args", "void", ["number", "number"]],
     // These two need to be lazy because they may be missing
@@ -192,7 +191,6 @@ export interface t_Cwraps {
     mono_wasm_intern_string_ref(strRef: MonoStringRef): void;
 
     mono_wasm_exit(exit_code: number): void;
-    mono_wasm_abort(): void;
     mono_wasm_getenv(name: string): CharPtr;
     mono_wasm_set_main_args(argc: number, argv: VoidPtr): void;
     mono_wasm_exec_regression(verbose_level: number, image: string): number;
diff --git a/src/mono/browser/runtime/driver.c b/src/mono/browser/runtime/driver.c
index bca878eb1820de..1b3663e4f3f3ca 100644
--- a/src/mono/browser/runtime/driver.c
+++ b/src/mono/browser/runtime/driver.c
@@ -347,12 +347,6 @@ mono_wasm_exit (int exit_code)
 	emscripten_force_exit (exit_code);
 }
 
-EMSCRIPTEN_KEEPALIVE int
-mono_wasm_abort ()
-{
-	abort ();
-}
-
 EMSCRIPTEN_KEEPALIVE void
 mono_wasm_set_main_args (int argc, char* argv[])
 {
diff --git a/src/mono/browser/runtime/invoke-js.ts b/src/mono/browser/runtime/invoke-js.ts
index 07c02c719d4851..fa6949f2d636aa 100644
--- a/src/mono/browser/runtime/invoke-js.ts
+++ b/src/mono/browser/runtime/invoke-js.ts
@@ -60,7 +60,7 @@ export function mono_wasm_invoke_jsimport_MT (signature: JSFunctionSignature, ar
                 }
                 return;
             } catch (ex2: any) {
-                runtimeHelpers.nativeExit(ex2);
+                runtimeHelpers.nativeAbort(ex2);
                 return;
             }
         }
diff --git a/src/mono/browser/runtime/loader/exit.ts b/src/mono/browser/runtime/loader/exit.ts
index 2264cd086fbd7a..cdea5d5997265b 100644
--- a/src/mono/browser/runtime/loader/exit.ts
+++ b/src/mono/browser/runtime/loader/exit.ts
@@ -39,36 +39,40 @@ export function uninstallUnhandledErrorHandler () {
     }
 }
 
+let originalOnAbort: ((reason: any, extraJson?:string)=>void)|undefined;
+let originalOnExit: ((code: number)=>void)|undefined;
+
 export function registerEmscriptenExitHandlers () {
-    if (!emscriptenModule.onAbort) {
-        emscriptenModule.onAbort = onAbort;
-    }
-    if (!emscriptenModule.onExit) {
-        emscriptenModule.onExit = onExit;
-    }
+    originalOnAbort = emscriptenModule.onAbort;
+    originalOnExit = emscriptenModule.onExit;
+    emscriptenModule.onAbort = onAbort;
+    emscriptenModule.onExit = onExit;
 }
 
 function unregisterEmscriptenExitHandlers () {
     if (emscriptenModule.onAbort == onAbort) {
-        emscriptenModule.onAbort = undefined;
+        emscriptenModule.onAbort = originalOnAbort;
     }
     if (emscriptenModule.onExit == onExit) {
-        emscriptenModule.onExit = undefined;
+        emscriptenModule.onExit = originalOnExit;
     }
 }
 function onExit (code: number) {
+    if (originalOnExit) {
+        originalOnExit(code);
+    }
     mono_exit(code, loaderHelpers.exitReason);
 }
 
 function onAbort (reason: any) {
-    mono_exit(1, loaderHelpers.exitReason || reason);
+    if (originalOnAbort) {
+        originalOnAbort(reason || loaderHelpers.exitReason);
+    }
+    mono_exit(1, reason || loaderHelpers.exitReason);
 }
 
 // this will also call mono_wasm_exit if available, which will call exitJS -> _proc_exit -> terminateAllThreads
 export function mono_exit (exit_code: number, reason?: any): void {
-    unregisterEmscriptenExitHandlers();
-    uninstallUnhandledErrorHandler();
-
     // unify shape of the reason object
     const is_object = reason && typeof reason === "object";
     exit_code = (is_object && typeof reason.status === "number")
@@ -82,7 +86,7 @@ export function mono_exit (exit_code: number, reason?: any): void {
     reason = is_object
         ? reason
         : (runtimeHelpers.ExitStatus
-            ? new runtimeHelpers.ExitStatus(exit_code)
+            ? createExitStatus(exit_code, message)
             : new Error("Exit with code " + exit_code + " " + message));
     reason.status = exit_code;
     if (!reason.message) {
@@ -90,15 +94,19 @@ export function mono_exit (exit_code: number, reason?: any): void {
     }
 
     // force stack property to be generated before we shut down managed code, or create current stack if it doesn't exist
-    if (!reason.stack) {
-        reason.stack = new Error().stack || "";
-    }
+    const stack = "" + (reason.stack || (new Error().stack));
+    Object.defineProperty(reason, "stack", {
+        get: () => stack
+    });
 
     // don't report this error twice
+    const alreadySilent = !!reason.silent;
     reason.silent = true;
 
     if (!is_exited()) {
         try {
+            unregisterEmscriptenExitHandlers();
+            uninstallUnhandledErrorHandler();
             if (!runtimeHelpers.runtimeReady) {
                 mono_log_debug("abort_startup, reason: " + reason);
                 abort_promises(reason);
@@ -119,19 +127,25 @@ export function mono_exit (exit_code: number, reason?: any): void {
         }
 
         try {
-            logOnExit(exit_code, reason);
-            appendElementOnExit(exit_code);
+            if (!alreadySilent) {
+                logOnExit(exit_code, reason);
+                appendElementOnExit(exit_code);
+            }
         } catch (err) {
             mono_log_warn("mono_exit failed", err);
             // don't propagate any failures
         }
 
         loaderHelpers.exitCode = exit_code;
-        loaderHelpers.exitReason = reason.message;
+        if (!loaderHelpers.exitReason) {
+            loaderHelpers.exitReason = reason;
+        }
 
         if (!ENVIRONMENT_IS_WORKER && runtimeHelpers.runtimeReady) {
             emscriptenModule.runtimeKeepalivePop();
         }
+    } else {
+        mono_log_debug("mono_exit called after exit");
     }
 
     if (loaderHelpers.config && loaderHelpers.config.asyncFlushOnExit && exit_code === 0) {
@@ -154,13 +168,11 @@ export function mono_exit (exit_code: number, reason?: any): void {
 function set_exit_code_and_quit_now (exit_code: number, reason?: any): void {
     if (WasmEnableThreads && ENVIRONMENT_IS_WORKER && runtimeHelpers.runtimeReady && runtimeHelpers.nativeAbort) {
         // note that the reason is not passed to UI thread
-        runtimeHelpers.runtimeReady = false;
         runtimeHelpers.nativeAbort(reason);
         throw reason;
     }
 
     if (runtimeHelpers.runtimeReady && runtimeHelpers.nativeExit) {
-        runtimeHelpers.runtimeReady = false;
         try {
             runtimeHelpers.nativeExit(exit_code);
         } catch (error: any) {
@@ -205,7 +217,6 @@ async function flush_node_streams () {
 }
 
 function abort_promises (reason: any) {
-    loaderHelpers.exitReason = reason;
     loaderHelpers.allDownloadsQueued.promise_control.reject(reason);
     loaderHelpers.afterConfigLoaded.promise_control.reject(reason);
     loaderHelpers.wasmCompilePromise.promise_control.reject(reason);
@@ -256,7 +267,7 @@ function logOnExit (exit_code: number, reason: any) {
             }
         }
     }
-    if (loaderHelpers.config) {
+    if (!ENVIRONMENT_IS_WORKER && loaderHelpers.config) {
         if (loaderHelpers.config.logExitCode) {
             if (loaderHelpers.config.forwardConsoleLogsToWS) {
                 teardown_proxy_console("WASM EXIT " + exit_code);
@@ -294,3 +305,10 @@ function fatal_handler (event: any, reason: any, type: string) {
         // no not re-throw from the fatal handler
     }
 }
+
+function createExitStatus (status:number, message:string) {
+    const ex = new runtimeHelpers.ExitStatus(status);
+    ex.message = message;
+    ex.toString = () => message;
+    return ex;
+}
diff --git a/src/mono/browser/runtime/loader/logging.ts b/src/mono/browser/runtime/loader/logging.ts
index 668d7e667b6d3f..ce98616e08d0ee 100644
--- a/src/mono/browser/runtime/loader/logging.ts
+++ b/src/mono/browser/runtime/loader/logging.ts
@@ -42,9 +42,6 @@ export function mono_log_error (msg: string, ...data: any) {
         if (data[0].silent) {
             return;
         }
-        if (data[0].toString) {
-            console.error(prefix + msg, data[0].toString());
-        }
         if (data[0].toString) {
             console.error(prefix + msg, data[0].toString());
             return;
@@ -118,12 +115,13 @@ export function setup_proxy_console (id: string, console: Console, origin: strin
 }
 
 export function teardown_proxy_console (message?: string) {
+    let counter = 30;
     const stop_when_ws_buffer_empty = () => {
         if (!consoleWebSocket) {
             if (message && originalConsoleMethods) {
                 originalConsoleMethods.log(message);
             }
-        } else if (consoleWebSocket.bufferedAmount == 0) {
+        } else if (consoleWebSocket.bufferedAmount == 0 || counter == 0) {
             if (message) {
                 // tell xharness WasmTestMessagesProcessor we are done.
                 // note this sends last few bytes into the same WS
@@ -136,6 +134,7 @@ export function teardown_proxy_console (message?: string) {
             consoleWebSocket.close(1000, message);
             (consoleWebSocket as any) = undefined;
         } else {
+            counter--;
             globalThis.setTimeout(stop_when_ws_buffer_empty, 100);
         }
     };
diff --git a/src/mono/browser/runtime/logging.ts b/src/mono/browser/runtime/logging.ts
index 1d91a870d52fb4..3558f24dc6c018 100644
--- a/src/mono/browser/runtime/logging.ts
+++ b/src/mono/browser/runtime/logging.ts
@@ -1,8 +1,10 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-/* eslint-disable no-console */
-import { INTERNAL, runtimeHelpers, mono_assert } from "./globals";
+import WasmEnableThreads from "consts:wasmEnableThreads";
+
+import { threads_c_functions as tcwraps } from "./cwraps";
+import { INTERNAL, runtimeHelpers, mono_assert, loaderHelpers, ENVIRONMENT_IS_WORKER, Module } from "./globals";
 import { utf8ToString } from "./strings";
 import { CharPtr, VoidPtr } from "./types/emscripten";
 
@@ -12,6 +14,7 @@ export function set_thread_prefix (threadPrefix: string) {
     prefix = `[${threadPrefix}] MONO_WASM: `;
 }
 
+/* eslint-disable no-console */
 export function mono_log_debug (msg: string, ...data: any) {
     if (runtimeHelpers.diagnosticTracing) {
         console.debug(prefix + msg, ...data);
@@ -27,9 +30,15 @@ export function mono_log_warn (msg: string, ...data: any) {
 }
 
 export function mono_log_error (msg: string, ...data: any) {
-    if (data && data.length > 0 && data[0] && typeof data[0] === "object" && data[0].silent) {
+    if (data && data.length > 0 && data[0] && typeof data[0] === "object") {
         // don't log silent errors
-        return;
+        if (data[0].silent) {
+            return;
+        }
+        if (data[0].toString) {
+            console.error(prefix + msg, data[0].toString());
+            return;
+        }
     }
     console.error(prefix + msg, ...data);
 }
@@ -123,7 +132,27 @@ export function mono_wasm_trace_logger (log_domain_ptr: CharPtr, log_level_ptr:
     switch (log_level) {
         case "critical":
         case "error":
-            console.error(mono_wasm_stringify_as_error_with_stack(message));
+            {
+                const messageWithStack = message + "\n" + (new Error().stack);
+                if (!loaderHelpers.exitReason) {
+                    loaderHelpers.exitReason = messageWithStack;
+                }
+                console.error(mono_wasm_stringify_as_error_with_stack(messageWithStack));
+                if (WasmEnableThreads) {
+                    try {
+                        tcwraps.mono_wasm_print_thread_dump();
+                    } catch (e) {
+                        console.error("Failed to print thread dump", e);
+                    }
+                }
+                if (WasmEnableThreads && ENVIRONMENT_IS_WORKER) {
+                    setTimeout(() => {
+                        mono_log_error("forcing abort 3000ms after last error log message", messageWithStack);
+                        // _emscripten_force_exit is proxied to UI thread and should also arrive in spin wait loop
+                        Module._emscripten_force_exit(1);
+                    }, 3000);
+                }
+            }
             break;
         case "warning":
             console.warn(message);
diff --git a/src/mono/browser/runtime/managed-exports.ts b/src/mono/browser/runtime/managed-exports.ts
index 065136faaba476..79bbcbf2af964c 100644
--- a/src/mono/browser/runtime/managed-exports.ts
+++ b/src/mono/browser/runtime/managed-exports.ts
@@ -14,7 +14,7 @@ import { assert_c_interop, assert_js_interop } from "./invoke-js";
 import { monoThreadInfo, mono_wasm_main_thread_ptr } from "./pthreads";
 import { _zero_region, copyBytes } from "./memory";
 import { stringToUTF8Ptr } from "./strings";
-import { mono_log_debug } from "./logging";
+import { mono_log_error } from "./logging";
 
 const managedExports: ManagedExports = {} as any;
 
@@ -269,7 +269,7 @@ export function install_main_synchronization_context (jsThreadBlockingMode: JSTh
         }
         return get_arg_gc_handle(res) as any;
     } catch (e) {
-        mono_log_debug("install_main_synchronization_context failed", e);
+        mono_log_error("install_main_synchronization_context failed", e);
         throw e;
     }
 }
diff --git a/src/mono/browser/runtime/pthreads/index.ts b/src/mono/browser/runtime/pthreads/index.ts
index 25f981d3f3b697..87ed1b006184db 100644
--- a/src/mono/browser/runtime/pthreads/index.ts
+++ b/src/mono/browser/runtime/pthreads/index.ts
@@ -5,13 +5,14 @@ import { mono_log_warn } from "../logging";
 import { utf16ToString } from "../strings";
 
 export {
-    mono_wasm_main_thread_ptr, mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop,
+    mono_wasm_main_thread_ptr,
     mono_wasm_pthread_ptr, update_thread_info, isMonoThreadMessage, monoThreadInfo,
 } from "./shared";
+export { mono_wasm_install_js_worker_interop, mono_wasm_uninstall_js_worker_interop } from "./worker-interop";
 export {
-    mono_wasm_dump_threads, cancelThreads,
+    mono_wasm_dump_threads, postCancelThreads,
     populateEmscriptenPool, mono_wasm_init_threads,
-    waitForThread, replaceEmscriptenPThreadUI
+    waitForThread, replaceEmscriptenPThreadUI, terminateAllThreads,
 } from "./ui-thread";
 export {
     mono_wasm_pthread_on_pthread_attached, mono_wasm_pthread_on_pthread_unregistered,
diff --git a/src/mono/browser/runtime/pthreads/shared.ts b/src/mono/browser/runtime/pthreads/shared.ts
index f72804fbcf873c..c266c2dfec66e4 100644
--- a/src/mono/browser/runtime/pthreads/shared.ts
+++ b/src/mono/browser/runtime/pthreads/shared.ts
@@ -6,11 +6,9 @@ import BuildConfiguration from "consts:configuration";
 
 import type { GCHandle, MonoThreadMessage, PThreadInfo, PThreadPtr } from "../types/internal";
 
-import { ENVIRONMENT_IS_PTHREAD, Module, loaderHelpers, mono_assert, runtimeHelpers } from "../globals";
+import { Module, loaderHelpers, runtimeHelpers } from "../globals";
 import { set_thread_prefix } from "../logging";
-import { bindings_init } from "../startup";
-import { forceDisposeProxies } from "../gc-handles";
-import { monoMessageSymbol, GCHandleNull, PThreadPtrNull, WorkerToMainMessageType } from "../types/internal";
+import { monoMessageSymbol, PThreadPtrNull, WorkerToMainMessageType } from "../types/internal";
 import { threads_c_functions as tcwraps } from "../cwraps";
 import { forceThreadMemoryViewRefresh } from "../memory";
 
@@ -34,39 +32,6 @@ export function isMonoThreadMessage (x: unknown): x is MonoThreadMessage {
     return typeof (xmsg.type) === "string" && typeof (xmsg.cmd) === "string";
 }
 
-export function mono_wasm_install_js_worker_interop (context_gc_handle: GCHandle): void {
-    if (!WasmEnableThreads) return;
-    bindings_init();
-    mono_assert(!runtimeHelpers.proxyGCHandle, "JS interop should not be already installed on this worker.");
-    runtimeHelpers.proxyGCHandle = context_gc_handle;
-    if (ENVIRONMENT_IS_PTHREAD) {
-        runtimeHelpers.managedThreadTID = runtimeHelpers.currentThreadTID;
-        runtimeHelpers.isManagedRunningOnCurrentThread = true;
-    }
-    Module.runtimeKeepalivePush();
-    monoThreadInfo.isDirtyBecauseOfInterop = true;
-    update_thread_info();
-    if (ENVIRONMENT_IS_PTHREAD) {
-        postMessageToMain({
-            monoCmd: WorkerToMainMessageType.enabledInterop,
-            info: monoThreadInfo,
-        });
-    }
-}
-
-export function mono_wasm_uninstall_js_worker_interop (): void {
-    if (!WasmEnableThreads) return;
-    mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "JS interop is not installed on this worker.");
-    mono_assert(runtimeHelpers.proxyGCHandle, "JSSynchronizationContext is not installed on this worker.");
-
-    forceDisposeProxies(true, runtimeHelpers.diagnosticTracing);
-    Module.runtimeKeepalivePop();
-
-    runtimeHelpers.proxyGCHandle = GCHandleNull;
-    runtimeHelpers.mono_wasm_bindings_is_ready = false;
-    update_thread_info();
-}
-
 // this is just for Debug build of the runtime, making it easier to debug worker threads
 export function update_thread_info (): void {
     if (!WasmEnableThreads) return;
diff --git a/src/mono/browser/runtime/pthreads/ui-thread.ts b/src/mono/browser/runtime/pthreads/ui-thread.ts
index b5f4f72875ec12..0dc2b80c57e0f8 100644
--- a/src/mono/browser/runtime/pthreads/ui-thread.ts
+++ b/src/mono/browser/runtime/pthreads/ui-thread.ts
@@ -160,7 +160,7 @@ export async function mono_wasm_init_threads () {
 }
 
 // when we create threads with browser event loop, it's not able to be joined by mono's thread join during shutdown and blocks process exit
-export function cancelThreads () {
+export function postCancelThreads () {
     if (!WasmEnableThreads) return;
     const workers: PThreadWorker[] = getRunningWorkers();
     for (const worker of workers) {
@@ -313,6 +313,10 @@ export function getRunningWorkers (): PThreadWorker[] {
     return getModulePThread().runningWorkers;
 }
 
+export function terminateAllThreads (): void {
+    getModulePThread().terminateAllThreads();
+}
+
 export function loadWasmModuleToWorker (worker: PThreadWorker): Promise {
     return getModulePThread().loadWasmModuleToWorker(worker);
 }
diff --git a/src/mono/browser/runtime/pthreads/worker-interop.ts b/src/mono/browser/runtime/pthreads/worker-interop.ts
new file mode 100644
index 00000000000000..35a8ff61a8a1fa
--- /dev/null
+++ b/src/mono/browser/runtime/pthreads/worker-interop.ts
@@ -0,0 +1,45 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+import WasmEnableThreads from "consts:wasmEnableThreads";
+
+import type { GCHandle } from "../types/internal";
+
+import { ENVIRONMENT_IS_PTHREAD, Module, mono_assert, runtimeHelpers } from "../globals";
+import { bindings_init } from "../startup";
+import { forceDisposeProxies } from "../gc-handles";
+import { GCHandleNull, WorkerToMainMessageType } from "../types/internal";
+import { monoThreadInfo, postMessageToMain, update_thread_info } from "./shared";
+
+export function mono_wasm_install_js_worker_interop (context_gc_handle: GCHandle): void {
+    if (!WasmEnableThreads) return;
+    bindings_init();
+    mono_assert(!runtimeHelpers.proxyGCHandle, "JS interop should not be already installed on this worker.");
+    runtimeHelpers.proxyGCHandle = context_gc_handle;
+    if (ENVIRONMENT_IS_PTHREAD) {
+        runtimeHelpers.managedThreadTID = runtimeHelpers.currentThreadTID;
+        runtimeHelpers.isManagedRunningOnCurrentThread = true;
+    }
+    Module.runtimeKeepalivePush();
+    monoThreadInfo.isDirtyBecauseOfInterop = true;
+    update_thread_info();
+    if (ENVIRONMENT_IS_PTHREAD) {
+        postMessageToMain({
+            monoCmd: WorkerToMainMessageType.enabledInterop,
+            info: monoThreadInfo,
+        });
+    }
+}
+
+export function mono_wasm_uninstall_js_worker_interop (): void {
+    if (!WasmEnableThreads) return;
+    mono_assert(runtimeHelpers.mono_wasm_bindings_is_ready, "JS interop is not installed on this worker.");
+    mono_assert(runtimeHelpers.proxyGCHandle, "JSSynchronizationContext is not installed on this worker.");
+
+    forceDisposeProxies(true, runtimeHelpers.diagnosticTracing);
+    Module.runtimeKeepalivePop();
+
+    runtimeHelpers.proxyGCHandle = GCHandleNull;
+    runtimeHelpers.mono_wasm_bindings_is_ready = false;
+    update_thread_info();
+}
diff --git a/src/mono/browser/runtime/run.ts b/src/mono/browser/runtime/run.ts
index be412fb9b70230..21f5fa2131a34c 100644
--- a/src/mono/browser/runtime/run.ts
+++ b/src/mono/browser/runtime/run.ts
@@ -3,12 +3,12 @@
 
 import WasmEnableThreads from "consts:wasmEnableThreads";
 
-import { ENVIRONMENT_IS_NODE, Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals";
+import { ENVIRONMENT_IS_NODE, ENVIRONMENT_IS_WORKER, Module, loaderHelpers, mono_assert, runtimeHelpers } from "./globals";
 import { mono_wasm_wait_for_debugger } from "./debug";
 import { mono_wasm_set_main_args } from "./startup";
 import cwraps from "./cwraps";
-import { mono_log_info } from "./logging";
-import { cancelThreads } from "./pthreads";
+import { mono_log_error, mono_log_info, mono_wasm_stringify_as_error_with_stack } from "./logging";
+import { postCancelThreads, terminateAllThreads } from "./pthreads";
 import { call_entry_point } from "./managed-exports";
 
 /**
@@ -77,16 +77,35 @@ export async function mono_run_main (main_assembly_name?: string, args?: string[
 
 
 export function nativeExit (code: number) {
-    if (WasmEnableThreads) {
-        cancelThreads();
+    if (runtimeHelpers.runtimeReady) {
+        runtimeHelpers.runtimeReady = false;
+        if (WasmEnableThreads) {
+            postCancelThreads();
+        }
+        cwraps.mono_wasm_exit(code);
     }
-    cwraps.mono_wasm_exit(code);
 }
 
 export function nativeAbort (reason: any) {
     loaderHelpers.exitReason = reason;
-    if (!loaderHelpers.is_exited()) {
-        cwraps.mono_wasm_abort();
+    if (runtimeHelpers.runtimeReady) {
+        runtimeHelpers.runtimeReady = false;
+        if (WasmEnableThreads) {
+            if (!ENVIRONMENT_IS_WORKER) {
+                terminateAllThreads();
+            } else {
+                // just in case if the UI thread is blocked, we need to force exit
+                // if UI thread receives message from Module.abort below, this thread will be terminated earlier
+                setTimeout(() => {
+                    mono_log_error("forcing abort 3000ms after nativeAbort attempt", reason);
+                    // _emscripten_force_exit is proxied to UI thread and should also arrive in spin wait loop
+                    Module._emscripten_force_exit(1);
+                }, 3000);
+            }
+        }
+
+        const reasonString = mono_wasm_stringify_as_error_with_stack(reason);
+        Module.abort(reasonString);
     }
     throw reason;
 }
diff --git a/src/mono/browser/runtime/startup.ts b/src/mono/browser/runtime/startup.ts
index 22fd1b085ee80a..b89522caff198a 100644
--- a/src/mono/browser/runtime/startup.ts
+++ b/src/mono/browser/runtime/startup.ts
@@ -203,6 +203,8 @@ export function preRunWorker () {
     const mark = startMeasure();
     try {
         jiterpreter_allocate_tables(); // this will return quickly if already allocated
+        runtimeHelpers.nativeExit = nativeExit;
+        runtimeHelpers.nativeAbort = nativeAbort;
         runtimeHelpers.runtimeReady = true;
         // signal next stage
         runtimeHelpers.afterPreRun.promise_control.resolve();
diff --git a/src/mono/browser/runtime/types/internal.ts b/src/mono/browser/runtime/types/internal.ts
index 25dce9f66bb751..e5579bfeba0f8c 100644
--- a/src/mono/browser/runtime/types/internal.ts
+++ b/src/mono/browser/runtime/types/internal.ts
@@ -433,6 +433,8 @@ export declare interface EmscriptenModuleInternal {
     __emscripten_thread_init(pthread_ptr: PThreadPtr, isMainBrowserThread: number, isMainRuntimeThread: number, canBlock: number): void;
     print(message: string): void;
     printErr(message: string): void;
+    abort(reason: any): void;
+    _emscripten_force_exit(exit_code: number): void;
 }
 
 /// A PromiseController encapsulates a Promise together with easy access to its resolve and reject functions.
@@ -551,6 +553,7 @@ export interface PThreadLibrary {
     threadInitTLS: () => void,
     getNewWorker: () => PThreadWorker,
     returnWorkerToPool: (worker: PThreadWorker) => void,
+    terminateAllThreads: () => void,
 }
 
 export interface PThreadInfoMap {
diff --git a/src/mono/browser/test-main.js b/src/mono/browser/test-main.js
index 1feb21ef2f796b..f34be2644aebec 100644
--- a/src/mono/browser/test-main.js
+++ b/src/mono/browser/test-main.js
@@ -52,7 +52,7 @@ if (!ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WEB && typeof globalThis.crypto ===
 }
 
 if (ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_WORKER) {
-    console.log("Running at: " + globalThis.location.href);
+    console.log("Running '" + globalThis.navigator.userAgent + "' at: " + globalThis.location.href);
 }
 
 let v8args;