diff --git a/Examples/ActorOnWebWorker/Package.swift b/Examples/ActorOnWebWorker/Package.swift
new file mode 100644
index 000000000..711bf6461
--- /dev/null
+++ b/Examples/ActorOnWebWorker/Package.swift
@@ -0,0 +1,20 @@
+// swift-tools-version: 6.0
+
+import PackageDescription
+
+let package = Package(
+    name: "Example",
+    platforms: [.macOS("15"), .iOS("18"), .watchOS("11"), .tvOS("18"), .visionOS("2")],
+    dependencies: [
+        .package(path: "../../"),
+    ],
+    targets: [
+        .executableTarget(
+            name: "MyApp",
+            dependencies: [
+                .product(name: "JavaScriptKit", package: "JavaScriptKit"),
+                .product(name: "JavaScriptEventLoop", package: "JavaScriptKit"),
+            ]
+        ),
+    ]
+)
diff --git a/Examples/ActorOnWebWorker/README.md b/Examples/ActorOnWebWorker/README.md
new file mode 100644
index 000000000..c0c849962
--- /dev/null
+++ b/Examples/ActorOnWebWorker/README.md
@@ -0,0 +1,21 @@
+# WebWorker + Actor example
+
+Install Development Snapshot toolchain `DEVELOPMENT-SNAPSHOT-2024-07-08-a` or later from [swift.org/install](https://www.swift.org/install/) and run the following commands:
+
+```sh
+$ (
+  set -eo pipefail; \
+  V="$(swiftc --version | head -n1)"; \
+  TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+  curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+  jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"] | "swift sdk install \"\(.url)\" --checksum \"\(.checksum)\""' | sh -x
+)
+$ export SWIFT_SDK_ID=$(
+  V="$(swiftc --version | head -n1)"; \
+  TAG="$(curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/tag-by-version.json" | jq -e -r --arg v "$V" '.[$v] | .[-1]')"; \
+  curl -sL "https://raw.githubusercontent.com/swiftwasm/swift-sdk-index/refs/heads/main/v1/builds/$TAG.json" | \
+  jq -r '.["swift-sdks"]["wasm32-unknown-wasip1-threads"]["id"]'
+)
+$ ./build.sh
+$ npx serve
+```
diff --git a/Examples/ActorOnWebWorker/Sources/MyApp.swift b/Examples/ActorOnWebWorker/Sources/MyApp.swift
new file mode 100644
index 000000000..7d362d13e
--- /dev/null
+++ b/Examples/ActorOnWebWorker/Sources/MyApp.swift
@@ -0,0 +1,262 @@
+import JavaScriptEventLoop
+import JavaScriptKit
+
+// Simple full-text search service
+actor SearchService {
+    struct Error: Swift.Error, CustomStringConvertible {
+        let message: String
+
+        var description: String {
+            return self.message
+        }
+    }
+
+    let serialExecutor: any SerialExecutor
+
+    // Simple in-memory index: word -> positions
+    var index: [String: [Int]] = [:]
+    var originalContent: String = ""
+    lazy var console: JSValue = {
+        JSObject.global.console
+    }()
+
+    nonisolated var unownedExecutor: UnownedSerialExecutor {
+        return self.serialExecutor.asUnownedSerialExecutor()
+    }
+
+    init(serialExecutor: any SerialExecutor) {
+        self.serialExecutor = serialExecutor
+    }
+
+    // Utility function for fetch
+    func fetch(_ url: String) -> JSPromise {
+        let jsFetch = JSObject.global.fetch.function!
+        return JSPromise(jsFetch(url).object!)!
+    }
+
+    func fetchAndIndex(url: String) async throws {
+        let response = try await fetch(url).value()
+        if response.status != 200 {
+            throw Error(message: "Failed to fetch content")
+        }
+        let text = try await JSPromise(response.text().object!)!.value()
+        let content = text.string!
+        index(content)
+    }
+
+    func index(_ contents: String) {
+        self.originalContent = contents
+        self.index = [:]
+
+        // Simple tokenization and indexing
+        var position = 0
+        let words = contents.lowercased().split(whereSeparator: { !$0.isLetter && !$0.isNumber })
+
+        for word in words {
+            let wordStr = String(word)
+            if wordStr.count > 1 {  // Skip single-character words
+                if index[wordStr] == nil {
+                    index[wordStr] = []
+                }
+                index[wordStr]?.append(position)
+            }
+            position += 1
+        }
+
+        _ = console.log("Indexing complete with", index.count, "unique words")
+    }
+
+    func search(_ query: String) -> [SearchResult] {
+        let queryWords = query.lowercased().split(whereSeparator: { !$0.isLetter && !$0.isNumber })
+
+        if queryWords.isEmpty {
+            return []
+        }
+
+        var results: [SearchResult] = []
+
+        // Start with the positions of the first query word
+        guard let firstWord = queryWords.first,
+            let firstWordPositions = index[String(firstWord)]
+        else {
+            return []
+        }
+
+        for position in firstWordPositions {
+            // Extract context around this position
+            let words = originalContent.lowercased().split(whereSeparator: {
+                !$0.isLetter && !$0.isNumber
+            })
+            var contextWords: [String] = []
+
+            // Get words for context (5 words before, 10 words after)
+            let contextStart = max(0, position - 5)
+            let contextEnd = min(position + 10, words.count - 1)
+
+            if contextStart <= contextEnd && contextStart < words.count {
+                for i in contextStart...contextEnd {
+                    if i < words.count {
+                        contextWords.append(String(words[i]))
+                    }
+                }
+            }
+
+            let context = contextWords.joined(separator: " ")
+            results.append(SearchResult(position: position, context: context))
+        }
+
+        return results
+    }
+}
+
+struct SearchResult {
+    let position: Int
+    let context: String
+}
+
+@MainActor
+final class App {
+    private let document = JSObject.global.document
+    private let alert = JSObject.global.alert.function!
+
+    // UI elements
+    private var container: JSValue
+    private var urlInput: JSValue
+    private var indexButton: JSValue
+    private var searchInput: JSValue
+    private var searchButton: JSValue
+    private var statusElement: JSValue
+    private var resultsElement: JSValue
+
+    // Search service
+    private let service: SearchService
+
+    init(service: SearchService) {
+        self.service = service
+        container = document.getElementById("container")
+        urlInput = document.getElementById("urlInput")
+        indexButton = document.getElementById("indexButton")
+        searchInput = document.getElementById("searchInput")
+        searchButton = document.getElementById("searchButton")
+        statusElement = document.getElementById("status")
+        resultsElement = document.getElementById("results")
+        setupEventHandlers()
+    }
+
+    private func setupEventHandlers() {
+        indexButton.onclick = .object(JSClosure { [weak self] _ in
+            guard let self else { return .undefined }
+            self.performIndex()
+            return .undefined
+        })
+
+        searchButton.onclick = .object(JSClosure { [weak self] _ in
+            guard let self else { return .undefined }
+            self.performSearch()
+            return .undefined
+        })
+    }
+
+    private func performIndex() {
+        let url = urlInput.value.string!
+
+        if url.isEmpty {
+            alert("Please enter a URL")
+            return
+        }
+
+        updateStatus("Downloading and indexing content...")
+
+        Task { [weak self] in
+            guard let self else { return }
+            do {
+                try await self.service.fetchAndIndex(url: url)
+                await MainActor.run {
+                    self.updateStatus("Indexing complete!")
+                }
+            } catch {
+                await MainActor.run {
+                    self.updateStatus("Error: \(error)")
+                }
+            }
+        }
+    }
+
+    private func performSearch() {
+        let query = searchInput.value.string!
+
+        if query.isEmpty {
+            alert("Please enter a search query")
+            return
+        }
+
+        updateStatus("Searching...")
+
+        Task { [weak self] in
+            guard let self else { return }
+            let searchResults = await self.service.search(query)
+            await MainActor.run {
+                self.displaySearchResults(searchResults)
+            }
+        }
+    }
+
+    private func updateStatus(_ message: String) {
+        statusElement.innerText = .string(message)
+    }
+
+    private func displaySearchResults(_ results: [SearchResult]) {
+        statusElement.innerText = .string("Search complete! Found \(results.count) results.")
+        resultsElement.innerHTML = .string("")
+
+        if results.isEmpty {
+            var noResults = document.createElement("p")
+            noResults.innerText = .string("No results found.")
+            _ = resultsElement.appendChild(noResults)
+        } else {
+            // Display up to 10 results
+            for (index, result) in results.prefix(10).enumerated() {
+                var resultItem = document.createElement("div")
+                resultItem.style = .string(
+                    "padding: 10px; margin: 5px 0; background: #f5f5f5; border-left: 3px solid blue;"
+                )
+                resultItem.innerHTML = .string(
+                    "<strong>Result \(index + 1):</strong> \(result.context)")
+                _ = resultsElement.appendChild(resultItem)
+            }
+        }
+    }
+}
+
+@main struct Main {
+    @MainActor static var app: App?
+
+    static func main() {
+        JavaScriptEventLoop.installGlobalExecutor()
+        WebWorkerTaskExecutor.installGlobalExecutor()
+
+        Task {
+            // Create dedicated worker and search service
+            let dedicatedWorker = try await WebWorkerDedicatedExecutor()
+            let service = SearchService(serialExecutor: dedicatedWorker)
+            app = App(service: service)
+        }
+    }
+}
+
+#if canImport(wasi_pthread)
+    import wasi_pthread
+    import WASILibc
+
+    /// Trick to avoid blocking the main thread. pthread_mutex_lock function is used by
+    /// the Swift concurrency runtime.
+    @_cdecl("pthread_mutex_lock")
+    func pthread_mutex_lock(_ mutex: UnsafeMutablePointer<pthread_mutex_t>) -> Int32 {
+        // DO NOT BLOCK MAIN THREAD
+        var ret: Int32
+        repeat {
+            ret = pthread_mutex_trylock(mutex)
+        } while ret == EBUSY
+        return ret
+    }
+#endif
diff --git a/Examples/ActorOnWebWorker/build.sh b/Examples/ActorOnWebWorker/build.sh
new file mode 100755
index 000000000..c82a10c32
--- /dev/null
+++ b/Examples/ActorOnWebWorker/build.sh
@@ -0,0 +1,3 @@
+swift package --swift-sdk "${SWIFT_SDK_ID:-wasm32-unknown-wasip1-threads}" -c release \
+    plugin --allow-writing-to-package-directory \
+    js --use-cdn --output ./Bundle
diff --git a/Examples/ActorOnWebWorker/index.html b/Examples/ActorOnWebWorker/index.html
new file mode 100644
index 000000000..2797702e1
--- /dev/null
+++ b/Examples/ActorOnWebWorker/index.html
@@ -0,0 +1,31 @@
+<html>
+
+<head>
+  <meta charset="utf-8">
+  <title>WebWorker + Actor example</title>
+</head>
+
+<body>
+  <script type="module">
+    import { init } from "./Bundle/index.js"
+    init(fetch(new URL("./Bundle/main.wasm", import.meta.url)));
+  </script>
+  <h1>Full-text Search with Actor on Web Worker</h1>
+
+  <div id="container">
+    <input type="text" id="urlInput"
+      value="https://raw.githubusercontent.com/swiftlang/swift/refs/tags/swift-DEVELOPMENT-SNAPSHOT-2025-03-13-a/docs/SIL/Instructions.md"
+      placeholder="Enter URL with text to index"
+      style="width: 300px; padding: 8px; margin-right: 10px;">
+    <button id="indexButton" style="padding: 8px 15px; margin-right: 10px;">Download & Index</button>
+    <br>
+    <br>
+    <input type="text" id="searchInput" placeholder="Enter search query"
+      style="width: 300px; padding: 8px; margin-right: 10px;">
+    <button id="searchButton" style="padding: 8px 15px;">Search</button>
+    <p id="status">Ready</p>
+    <div id="results"></div>
+  </div>
+</body>
+
+</html>
diff --git a/Examples/ActorOnWebWorker/serve.json b/Examples/ActorOnWebWorker/serve.json
new file mode 100644
index 000000000..537a16904
--- /dev/null
+++ b/Examples/ActorOnWebWorker/serve.json
@@ -0,0 +1,14 @@
+{
+    "headers": [{
+        "source": "**/*",
+        "headers": [
+            {
+                "key": "Cross-Origin-Embedder-Policy",
+                "value": "require-corp"
+            }, {
+                "key": "Cross-Origin-Opener-Policy",
+                "value": "same-origin"
+            }
+        ]
+    }]
+}
diff --git a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
index 07eec2cd2..ce4fb1047 100644
--- a/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
+++ b/Sources/JavaScriptEventLoop/JavaScriptEventLoop.swift
@@ -232,6 +232,24 @@ public extension JSPromise {
         }
     }
 
+    /// Wait for the promise to complete, returning its result or exception as a Result.
+    ///
+    /// - Note: Calling this function does not switch from the caller's isolation domain.
+    func value(isolation: isolated (any Actor)? = #isolation) async throws -> JSValue {
+        try await withUnsafeThrowingContinuation(isolation: isolation) { [self] continuation in
+            self.then(
+                success: {
+                    continuation.resume(returning: $0)
+                    return JSValue.undefined
+                },
+                failure: {
+                    continuation.resume(throwing: JSException($0))
+                    return JSValue.undefined
+                }
+            )
+        }
+    }
+
     /// Wait for the promise to complete, returning its result or exception as a Result.
     var result: JSPromise.Result {
         get async {
diff --git a/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift
new file mode 100644
index 000000000..695eb9c61
--- /dev/null
+++ b/Sources/JavaScriptEventLoop/WebWorkerDedicatedExecutor.swift
@@ -0,0 +1,60 @@
+import JavaScriptKit
+import _CJavaScriptEventLoop
+
+#if canImport(Synchronization)
+    import Synchronization
+#endif
+#if canImport(wasi_pthread)
+    import wasi_pthread
+    import WASILibc
+#endif
+
+/// A serial executor that runs on a dedicated web worker thread.
+///
+/// This executor is useful for running actors on a dedicated web worker thread.
+///
+/// ## Usage
+///
+/// ```swift
+/// actor MyActor {
+///     let executor: WebWorkerDedicatedExecutor
+///     nonisolated var unownedExecutor: UnownedSerialExecutor {
+///         self.executor.asUnownedSerialExecutor()
+///     }
+///     init(executor: WebWorkerDedicatedExecutor) {
+///         self.executor = executor
+///     }
+/// }
+///
+/// let executor = try await WebWorkerDedicatedExecutor()
+/// let actor = MyActor(executor: executor)
+/// ```
+///
+/// - SeeAlso: ``WebWorkerTaskExecutor``
+@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
+public final class WebWorkerDedicatedExecutor: SerialExecutor {
+
+    private let underlying: WebWorkerTaskExecutor
+
+    /// - Parameters:
+    ///   - timeout: The maximum time to wait for all worker threads to be started. Default is 3 seconds.
+    ///   - checkInterval: The interval to check if all worker threads are started. Default is 5 microseconds.
+    /// - Throws: An error if any worker thread fails to initialize within the timeout period.
+    public init(timeout: Duration = .seconds(3), checkInterval: Duration = .microseconds(5)) async throws {
+        let underlying = try await WebWorkerTaskExecutor(
+            numberOfThreads: 1, timeout: timeout, checkInterval: checkInterval
+        )
+        self.underlying = underlying
+    }
+
+    /// Terminates the worker thread.
+    public func terminate() {
+        self.underlying.terminate()
+    }
+
+    // MARK: - SerialExecutor conformance
+
+    public func enqueue(_ job: consuming ExecutorJob) {
+        self.underlying.enqueue(job)
+    }
+}
diff --git a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift
index 4c441f3c4..0582fe8c4 100644
--- a/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift
+++ b/Sources/JavaScriptEventLoopTestSupport/JavaScriptEventLoopTestSupport.swift
@@ -27,6 +27,11 @@ import JavaScriptEventLoop
 func swift_javascriptkit_activate_js_executor_impl() {
     MainActor.assumeIsolated {
         JavaScriptEventLoop.installGlobalExecutor()
+        #if canImport(wasi_pthread) && compiler(>=6.1) && _runtime(_multithreaded)
+        if #available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) {
+            WebWorkerTaskExecutor.installGlobalExecutor()
+        }
+        #endif
     }
 }
 
diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift
new file mode 100644
index 000000000..b6c2bd8db
--- /dev/null
+++ b/Tests/JavaScriptEventLoopTests/WebWorkerDedicatedExecutorTests.swift
@@ -0,0 +1,34 @@
+#if compiler(>=6.1) && _runtime(_multithreaded)
+import XCTest
+@testable import JavaScriptEventLoop
+
+final class WebWorkerDedicatedExecutorTests: XCTestCase {
+    actor MyActor {
+        let executor: WebWorkerDedicatedExecutor
+        nonisolated var unownedExecutor: UnownedSerialExecutor {
+            self.executor.asUnownedSerialExecutor()
+        }
+
+        init(executor: WebWorkerDedicatedExecutor) {
+            self.executor = executor
+            XCTAssertTrue(isMainThread())
+        }
+
+        func onWorkerThread() async {
+            XCTAssertFalse(isMainThread())
+            await Task.detached {}.value
+            // Should keep on the thread after back from the other isolation domain
+            XCTAssertFalse(isMainThread())
+        }
+    }
+
+    func testEnqueue() async throws {
+        let executor = try await WebWorkerDedicatedExecutor()
+        defer { executor.terminate() }
+        let actor = MyActor(executor: executor)
+        XCTAssertTrue(isMainThread())
+        await actor.onWorkerThread()
+        XCTAssertTrue(isMainThread())
+    }
+}
+#endif
diff --git a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
index 0dfdac25f..1696224df 100644
--- a/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
+++ b/Tests/JavaScriptEventLoopTests/WebWorkerTaskExecutorTests.swift
@@ -23,10 +23,6 @@ func pthread_mutex_lock(_ mutex: UnsafeMutablePointer<pthread_mutex_t>) -> Int32
 #endif
 
 final class WebWorkerTaskExecutorTests: XCTestCase {
-    override func setUp() async {
-        WebWorkerTaskExecutor.installGlobalExecutor()
-    }
-
     func testTaskRunOnMainThread() async throws {
         let executor = try await WebWorkerTaskExecutor(numberOfThreads: 1)