Revert "[WGPUFuture] Implement pop error scope in wire/native with Futures."

This reverts commit 97dfc182410301992db9315de11c544c4c3bf087.

Reason for revert: Breaks dawn/node - all tests fail with `endTestScope timed out`

Original change's description:
> [WGPUFuture] Implement pop error scope in wire/native with Futures.
>
> - Fixes some tests that now require us to have access to the Instance
>   for calling ProcessEvents.
> - Also fixes some tests to call ProcessEvents over Tick.
>
> Bug: dawn:1987
> Change-Id: I572dc349f9c1adf9a13be267a22eaa73d69e6def
> Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/169140
> Kokoro: Kokoro <noreply+kokoro@google.com>
> Commit-Queue: Loko Kung <lokokung@google.com>
> Reviewed-by: Shrek Shao <shrekshao@google.com>
> Reviewed-by: Austin Eng <enga@chromium.org>

TBR=enga@chromium.org,shrekshao@google.com,noreply+kokoro@google.com,dawn-scoped@luci-project-accounts.iam.gserviceaccount.com,lokokung@google.com

Change-Id: I92621fb36f5e0dbaf78f914dffac5239ca59c5e1
No-Presubmit: true
No-Tree-Checks: true
No-Try: true
Bug: dawn:1987
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/174021
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Kokoro: Ben Clayton <bclayton@google.com>
Reviewed-by: Ben Clayton <bclayton@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/dawn/dawn.json b/src/dawn/dawn.json
index b5c1530..7112745 100644
--- a/src/dawn/dawn.json
+++ b/src/dawn/dawn.json
@@ -1428,15 +1428,6 @@
                 ]
             },
             {
-                "name": "pop error scope f",
-                "_comment": "TODO(crbug.com/dawn/2021): This is dawn/emscripten-only until we rename it to replace the old API. See bug for details.",
-                "tags": ["dawn", "emscripten"],
-                "returns": "future",
-                "args": [
-                    {"name": "callback info", "type": "pop error scope callback info"}
-                ]
-            },
-            {
                 "name": "set label",
                 "returns": "void",
                 "args": [
@@ -1487,33 +1478,6 @@
             {"name": "userdata", "type": "void *"}
         ]
     },
-    "pop error scope status": {
-        "category": "enum",
-        "emscripten_no_enum_table": true,
-        "values": [
-            {"value": 0, "name": "success"},
-            {"value": 1, "name": "instance dropped"}
-        ]
-    },
-    "pop error scope callback": {
-        "category": "function pointer",
-        "args": [
-            {"name": "status", "type": "pop error scope status"},
-            {"name": "type", "type": "error type"},
-            {"name": "message", "type": "char", "annotation": "const*", "length": "strlen"},
-            {"name": "userdata", "type": "void *"}
-        ]
-    },
-    "pop error scope callback info": {
-        "category": "structure",
-        "extensible": "in",
-        "members": [
-            {"name": "mode", "type": "callback mode"},
-            {"name": "callback", "type": "pop error scope callback"},
-            {"name": "old callback", "type": "error callback", "_comment": "TODO(crbug.com/dawn/2021) Deprecate this field once we have moved callers to use the new callback signature."},
-            {"name": "userdata", "type": "void *", "default": "nullptr"}
-        ]
-    },
     "limits": {
         "category": "structure",
         "members": [
diff --git a/src/dawn/dawn_wire.json b/src/dawn/dawn_wire.json
index 9365631..48c931d 100644
--- a/src/dawn/dawn_wire.json
+++ b/src/dawn/dawn_wire.json
@@ -71,8 +71,7 @@
         ],
         "device pop error scope": [
             { "name": "device id", "type": "ObjectId", "id_type": "device" },
-            { "name": "event manager handle", "type": "ObjectHandle" },
-            { "name": "future", "type": "future" }
+            { "name": "request serial", "type": "uint64_t" }
         ],
         "destroy object": [
             { "name": "object type", "type": "ObjectType" },
@@ -153,8 +152,8 @@
             { "name": "message", "type": "char", "annotation": "const*", "length": "strlen" }
         ],
         "device pop error scope callback": [
-            { "name": "event manager", "type": "ObjectHandle" },
-            { "name": "future", "type": "future" },
+            { "name": "device", "type": "ObjectHandle", "handle_type": "device" },
+            { "name": "request serial", "type": "uint64_t" },
             { "name": "type", "type": "error type" },
             { "name": "message", "type": "char", "annotation": "const*", "length": "strlen" }
         ],
@@ -223,7 +222,6 @@
             "DeviceHasFeature",
             "DeviceEnumerateFeatures",
             "DevicePopErrorScope",
-            "DevicePopErrorScopeF",
             "DeviceSetDeviceLostCallback",
             "DeviceSetUncapturedErrorCallback",
             "DeviceSetLoggingCallback",
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index 14e31a0..6af75e2 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -726,70 +726,25 @@
 }
 
 void DeviceBase::APIPopErrorScope(wgpu::ErrorCallback callback, void* userdata) {
-    static wgpu::ErrorCallback kDefaultCallback = [](WGPUErrorType, char const*, void*) {};
-
-    PopErrorScopeCallbackInfo callbackInfo = {};
-    callbackInfo.mode = wgpu::CallbackMode::AllowProcessEvents;
-    callbackInfo.oldCallback = callback != nullptr ? callback : kDefaultCallback;
-    callbackInfo.userdata = userdata;
-    APIPopErrorScopeF(callbackInfo);
-}
-
-Future DeviceBase::APIPopErrorScopeF(const PopErrorScopeCallbackInfo& callbackInfo) {
-    struct PopErrorScopeEvent final : public EventManager::TrackedEvent {
-        // TODO(crbug.com/dawn/2021) Remove the old callback type.
-        WGPUPopErrorScopeCallback mCallback;
-        WGPUErrorCallback mOldCallback;
-        void* mUserdata;
-        std::optional<ErrorScope> mScope;
-
-        PopErrorScopeEvent(const PopErrorScopeCallbackInfo& callbackInfo,
-                           std::optional<ErrorScope>&& scope)
-            : TrackedEvent(callbackInfo.mode, TrackedEvent::Completed{}),
-              mCallback(callbackInfo.callback),
-              mOldCallback(callbackInfo.oldCallback),
-              mUserdata(callbackInfo.userdata),
-              mScope(scope) {
-            // Exactly 1 callback should be set.
-            DAWN_ASSERT((mCallback != nullptr && mOldCallback == nullptr) ||
-                        (mCallback == nullptr && mOldCallback != nullptr));
-            CompleteIfSpontaneous();
-        }
-
-        ~PopErrorScopeEvent() override { EnsureComplete(EventCompletionType::Shutdown); }
-
-        void Complete(EventCompletionType completionType) override {
-            WGPUPopErrorScopeStatus status = completionType == EventCompletionType::Ready
-                                                 ? WGPUPopErrorScopeStatus_Success
-                                                 : WGPUPopErrorScopeStatus_InstanceDropped;
-            WGPUErrorType type;
-            const char* message;
-            if (mScope) {
-                type = static_cast<WGPUErrorType>(mScope->GetErrorType());
-                message = mScope->GetErrorMessage().c_str();
-            } else {
-                type = WGPUErrorType_Unknown;
-                message = "No error scopes to pop";
-            }
-
-            if (mCallback) {
-                mCallback(status, type, message, mUserdata);
-            } else {
-                mOldCallback(type, message, mUserdata);
-            }
-        }
-    };
-
-    std::optional<ErrorScope> scope;
-    if (IsLost()) {
-        scope = ErrorScope(wgpu::ErrorType::DeviceLost, "GPU device disconnected");
-    } else if (!mErrorScopeStack->Empty()) {
-        scope = mErrorScopeStack->Pop();
+    if (callback == nullptr) {
+        static wgpu::ErrorCallback defaultCallback = [](WGPUErrorType, char const*, void*) {};
+        callback = defaultCallback;
     }
-
-    FutureID futureID = GetInstance()->GetEventManager()->TrackEvent(
-        callbackInfo.mode, AcquireRef(new PopErrorScopeEvent(callbackInfo, std::move(scope))));
-    return {futureID};
+    if (IsLost()) {
+        mCallbackTaskManager->AddCallbackTask(
+            std::bind(callback, WGPUErrorType_DeviceLost, "GPU device disconnected", userdata));
+        return;
+    }
+    if (mErrorScopeStack->Empty()) {
+        mCallbackTaskManager->AddCallbackTask(
+            std::bind(callback, WGPUErrorType_Unknown, "No error scopes to pop", userdata));
+        return;
+    }
+    ErrorScope scope = mErrorScopeStack->Pop();
+    mCallbackTaskManager->AddCallbackTask(
+        [callback, errorType = static_cast<WGPUErrorType>(scope.GetErrorType()),
+         message = scope.GetErrorMessage(),
+         userdata] { callback(errorType, message.c_str(), userdata); });
 }
 
 BlobCache* DeviceBase::GetBlobCache() const {
@@ -1407,8 +1362,6 @@
 
 // Returns true if future ticking is needed.
 bool DeviceBase::APITick() {
-    // TODO(dawn:1987) Add deprecation warning when Instance.ProcessEvents no longer calls this.
-
     // Tick may trigger callbacks which drop a ref to the device itself. Hold a Ref to ourselves
     // to avoid deleting |this| in the middle of this function call.
     Ref<DeviceBase> self(this);
diff --git a/src/dawn/native/Device.h b/src/dawn/native/Device.h
index e833ac2..70b2df9 100644
--- a/src/dawn/native/Device.h
+++ b/src/dawn/native/Device.h
@@ -328,7 +328,6 @@
     void APISetLoggingCallback(wgpu::LoggingCallback callback, void* userdata);
     void APIPushErrorScope(wgpu::ErrorFilter filter);
     void APIPopErrorScope(wgpu::ErrorCallback callback, void* userdata);
-    Future APIPopErrorScopeF(const PopErrorScopeCallbackInfo& callbackInfo);
 
     MaybeError ValidateIsAlive() const;
 
diff --git a/src/dawn/native/ErrorScope.cpp b/src/dawn/native/ErrorScope.cpp
index 63ab77d..6c21e40 100644
--- a/src/dawn/native/ErrorScope.cpp
+++ b/src/dawn/native/ErrorScope.cpp
@@ -52,9 +52,6 @@
 ErrorScope::ErrorScope(wgpu::ErrorFilter errorFilter)
     : mMatchedErrorType(ErrorFilterToErrorType(errorFilter)) {}
 
-ErrorScope::ErrorScope(wgpu::ErrorType error, std::string_view message)
-    : mMatchedErrorType(error), mCapturedError(error), mErrorMessage(message) {}
-
 wgpu::ErrorType ErrorScope::GetErrorType() const {
     return mCapturedError;
 }
diff --git a/src/dawn/native/ErrorScope.h b/src/dawn/native/ErrorScope.h
index f40934a..4032b47 100644
--- a/src/dawn/native/ErrorScope.h
+++ b/src/dawn/native/ErrorScope.h
@@ -37,8 +37,6 @@
 
 class ErrorScope {
   public:
-    ErrorScope(wgpu::ErrorType error, std::string_view message);
-
     wgpu::ErrorType GetErrorType() const;
     const std::string& GetErrorMessage() const;
 
diff --git a/src/dawn/tests/DawnTest.h b/src/dawn/tests/DawnTest.h
index e625a5b..474d9e1 100644
--- a/src/dawn/tests/DawnTest.h
+++ b/src/dawn/tests/DawnTest.h
@@ -127,7 +127,7 @@
     EXPECT_CALL(mDeviceErrorCallback,                                             \
                 Call(testing::Ne(WGPUErrorType_NoError), matcher, device.Get())); \
     statement;                                                                    \
-    instance.ProcessEvents();                                                     \
+    device.Tick();                                                                \
     FlushWire();                                                                  \
     testing::Mock::VerifyAndClearExpectations(&mDeviceErrorCallback);             \
     do {                                                                          \
diff --git a/src/dawn/tests/end2end/DeviceLifetimeTests.cpp b/src/dawn/tests/end2end/DeviceLifetimeTests.cpp
index 02ce3a6..4ddb8f7 100644
--- a/src/dawn/tests/end2end/DeviceLifetimeTests.cpp
+++ b/src/dawn/tests/end2end/DeviceLifetimeTests.cpp
@@ -99,19 +99,17 @@
 // Test that the device can be dropped while a popErrorScope callback is in flight.
 TEST_P(DeviceLifetimeTests, DroppedWhilePopErrorScope) {
     device.PushErrorScope(wgpu::ErrorFilter::Validation);
-    bool done = false;
-
+    bool wire = UsesWire();
     device.PopErrorScope(
         [](WGPUErrorType type, const char*, void* userdata) {
-            *static_cast<bool*>(userdata) = true;
-            EXPECT_EQ(type, WGPUErrorType_NoError);
+            const bool wire = *static_cast<bool*>(userdata);
+            // On the wire, all callbacks get rejected immediately with once the device is deleted.
+            // In native, popErrorScope is called synchronously.
+            // TODO(crbug.com/dawn/1122): These callbacks should be made consistent.
+            EXPECT_EQ(type, wire ? WGPUErrorType_Unknown : WGPUErrorType_NoError);
         },
-        &done);
+        &wire);
     device = nullptr;
-
-    while (!done) {
-        WaitABit();
-    }
 }
 
 // Test that the device can be dropped inside an onSubmittedWorkDone callback.
@@ -133,6 +131,11 @@
         &data);
 
     while (!data.done) {
+        // WaitABit no longer can call tick since we've moved the device from the fixture into the
+        // userdata.
+        if (data.device) {
+            data.device.Tick();
+        }
         WaitABit();
     }
 }
diff --git a/src/dawn/tests/end2end/EventTests.cpp b/src/dawn/tests/end2end/EventTests.cpp
index 10de0c1..d152975c 100644
--- a/src/dawn/tests/end2end/EventTests.cpp
+++ b/src/dawn/tests/end2end/EventTests.cpp
@@ -151,7 +151,7 @@
                     Call(WGPUDeviceLostReason_Undefined, testing::_, testing::_))
             .Times(1);
         testDevice.ForceLoss(wgpu::DeviceLostReason::Undefined, "Device lost for testing");
-        testInstance.ProcessEvents();
+        testDevice.Tick();
     }
 
     void TrivialSubmit() {
@@ -283,6 +283,7 @@
 
                     uint64_t oldCompletionCount = mCallbacksCompletedCount;
                     FlushWire();
+                    testDevice.Tick();
                     auto status = testInstance.WaitAny(mFutures.size(), mFutures.data(), 0);
                     if (status == wgpu::WaitStatus::TimedOut) {
                         continue;
@@ -304,6 +305,7 @@
                     ASSERT_FALSE(testTimeExceeded());
 
                     FlushWire();
+                    testDevice.Tick();
                     testInstance.ProcessEvents();
 
                     if (loopOnlyOnce) {
@@ -356,14 +358,9 @@
 TEST_P(EventCompletionTests, WorkDoneAfterDeviceLoss) {
     TrivialSubmit();
     LoseTestDevice();
-    // Tracking and waiting need to be done together w.r.t the device error assertion because error
-    // assertion in DawnTest.h currently calls ProcessEvents which will cause the work done event to
-    // trigger before the TestWaitAll call.
-    auto TestF = [&]() {
-        TrackForTest(OnSubmittedWorkDone(WGPUQueueWorkDoneStatus_Success));
-        TestWaitAll();
-    };
-    ASSERT_DEVICE_ERROR_ON(testDevice, TestF());
+    ASSERT_DEVICE_ERROR_ON(testDevice,
+                           TrackForTest(OnSubmittedWorkDone(WGPUQueueWorkDoneStatus_Success)));
+    TestWaitAll();
 }
 
 // WorkDone event twice after submitting some trivial work.
diff --git a/src/dawn/tests/end2end/MaxLimitTests.cpp b/src/dawn/tests/end2end/MaxLimitTests.cpp
index 1a86e32..d8810bc 100644
--- a/src/dawn/tests/end2end/MaxLimitTests.cpp
+++ b/src/dawn/tests/end2end/MaxLimitTests.cpp
@@ -221,7 +221,7 @@
         device.PopErrorScope([](WGPUErrorType type, const char*,
                                 void* userdata) { *static_cast<WGPUErrorType*>(userdata) = type; },
                              &oomResult);
-        instance.ProcessEvents();
+        device.Tick();
         FlushWire();
         // Max buffer size is smaller than the max buffer binding size.
         DAWN_TEST_UNSUPPORTED_IF(oomResult == WGPUErrorType_OutOfMemory);
diff --git a/src/dawn/tests/end2end/MultithreadTests.cpp b/src/dawn/tests/end2end/MultithreadTests.cpp
index d1ddeef..9a938eb 100644
--- a/src/dawn/tests/end2end/MultithreadTests.cpp
+++ b/src/dawn/tests/end2end/MultithreadTests.cpp
@@ -1144,7 +1144,7 @@
                 *error = true;
             },
             &errorThrown);
-        instance.ProcessEvents();
+        device.Tick();
         EXPECT_TRUE(errorThrown.load());
 
         // Second copy is valid.
diff --git a/src/dawn/tests/unittests/validation/ErrorScopeValidationTests.cpp b/src/dawn/tests/unittests/validation/ErrorScopeValidationTests.cpp
index 1f13910..8d239aa 100644
--- a/src/dawn/tests/unittests/validation/ErrorScopeValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/ErrorScopeValidationTests.cpp
@@ -60,9 +60,9 @@
 
 class ErrorScopeValidationTest : public ValidationTest {
   protected:
-    void FlushWireAndProcessEvents() {
+    void FlushWireAndTick() {
         FlushWire();
-        instance.ProcessEvents();
+        device.Tick();
     }
 
     // Generate new void* pointer for use as `userdata` in callback. The pointer
@@ -99,7 +99,7 @@
     EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_NoError, _, userdata))
         .Times(1);
     device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata);
-    FlushWireAndProcessEvents();
+    FlushWireAndTick();
 }
 
 // Test the simple case where the error scope catches an error.
@@ -114,7 +114,7 @@
     EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_Validation, _, userdata))
         .Times(1);
     device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata);
-    FlushWireAndProcessEvents();
+    FlushWireAndTick();
 }
 
 // Test that errors bubble to the parent scope if not handled by the current scope.
@@ -131,14 +131,14 @@
     EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_NoError, _, userdata_1))
         .Times(1);
     device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata_1);
-    FlushWireAndProcessEvents();
+    FlushWireAndTick();
 
     // Parent validation error scope captures the error.
     void* userdata_2 = CreateUserData();
     EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_Validation, _, userdata_2))
         .Times(1);
     device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata_2);
-    FlushWireAndProcessEvents();
+    FlushWireAndTick();
 }
 
 // Test that if an error scope matches an error, it does not bubble to the parent scope.
@@ -155,14 +155,14 @@
     EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_Validation, _, userdata_1))
         .Times(1);
     device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata_1);
-    FlushWireAndProcessEvents();
+    FlushWireAndTick();
 
     // Parent scope does not see the error.
     void* userdata_2 = CreateUserData();
     EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_NoError, _, userdata_2))
         .Times(1);
     device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata_2);
-    FlushWireAndProcessEvents();
+    FlushWireAndTick();
 }
 
 // Test that if no error scope handles an error, it goes to the device UncapturedError callback
@@ -177,7 +177,7 @@
     EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_NoError, _, userdata))
         .Times(1);
     device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata);
-    FlushWireAndProcessEvents();
+    FlushWireAndTick();
 }
 
 // Check that push/popping error scopes must be balanced.
@@ -188,7 +188,7 @@
         EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_Unknown, _, userdata))
             .Times(1);
         device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata);
-        FlushWireAndProcessEvents();
+        FlushWireAndTick();
     }
     // Too many pops
     {
@@ -198,13 +198,13 @@
         EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_NoError, _, userdata_1))
             .Times(1);
         device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata_1);
-        FlushWireAndProcessEvents();
+        FlushWireAndTick();
 
         void* userdata_2 = CreateUserData();
         EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_Unknown, _, userdata_2))
             .Times(1);
         device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata_2);
-        FlushWireAndProcessEvents();
+        FlushWireAndTick();
     }
 }
 
@@ -234,7 +234,8 @@
     device.PopErrorScope(errorScopeCallback1.Callback(),
                          errorScopeCallback1.MakeUserdata(userdata_3));
 
-    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_Success, userdata_1));
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_Success, userdata_1))
+        .InSequence(seq);
     WaitForAllOperations(device);
 }
 
@@ -249,14 +250,22 @@
         queue.Submit(0, nullptr);
     }
 
+    if (UsesWire()) {
+        void* userdata = CreateUserData();
+        device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata);
+
+        EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_Unknown, _, userdata))
+            .Times(1);
+        ExpectDeviceDestruction();
+        device = nullptr;
+    } else {
         void* userdata = CreateUserData();
         EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_NoError, _, userdata))
             .Times(1);
         device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata);
         ExpectDeviceDestruction();
         device = nullptr;
-
-        FlushWireAndProcessEvents();
+    }
 }
 
 // If the device is destroyed, pop error scope should callback with device lost.
@@ -264,13 +273,13 @@
     device.PushErrorScope(wgpu::ErrorFilter::Validation);
     ExpectDeviceDestruction();
     device.Destroy();
-    FlushWireAndProcessEvents();
+    FlushWireAndTick();
 
     void* userdata = CreateUserData();
     EXPECT_CALL(*mockDevicePopErrorScopeCallback, Call(WGPUErrorType_DeviceLost, _, userdata))
         .Times(1);
     device.PopErrorScope(ToMockDevicePopErrorScopeCallback, userdata);
-    FlushWireAndProcessEvents();
+    FlushWireAndTick();
 }
 
 // Regression test that on device shutdown, we don't get a recursion in O(pushed error scope) that
diff --git a/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp b/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
index 362fd83..1e6c588 100644
--- a/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/ShaderModuleValidationTests.cpp
@@ -736,7 +736,8 @@
 
     // Create testing adapter with the AllowUnsafeAPIs toggle explicitly enabled or disabled,
     // overriding the instance's toggle.
-    void CreateTestAdapterWithUnsafeAPIToggle(wgpu::RequestAdapterOptions options,
+    void CreateTestAdapterWithUnsafeAPIToggle(wgpu::Instance instance,
+                                              wgpu::RequestAdapterOptions options,
                                               bool allowUnsafeAPIs) {
         wgpu::DawnTogglesDescriptor deviceTogglesDesc{};
         options.nextInChain = &deviceTogglesDesc;
@@ -810,9 +811,9 @@
 class ShaderModuleExtensionValidationTestSafeNoFeature
     : public ShaderModuleExtensionValidationTestBase {
   protected:
-    void CreateTestAdapter(wgpu::RequestAdapterOptions options) override {
+    void CreateTestAdapter(wgpu::Instance instance, wgpu::RequestAdapterOptions options) override {
         // Create a safe adapter
-        CreateTestAdapterWithUnsafeAPIToggle(options, false);
+        CreateTestAdapterWithUnsafeAPIToggle(instance, options, false);
     }
     WGPUDevice CreateTestDevice(native::Adapter dawnAdapter,
                                 wgpu::DeviceDescriptor descriptor) override {
@@ -842,9 +843,9 @@
 class ShaderModuleExtensionValidationTestUnsafeNoFeature
     : public ShaderModuleExtensionValidationTestBase {
   protected:
-    void CreateTestAdapter(wgpu::RequestAdapterOptions options) override {
+    void CreateTestAdapter(wgpu::Instance instance, wgpu::RequestAdapterOptions options) override {
         // Create an unsafe adapter
-        CreateTestAdapterWithUnsafeAPIToggle(options, true);
+        CreateTestAdapterWithUnsafeAPIToggle(instance, options, true);
     }
     WGPUDevice CreateTestDevice(native::Adapter dawnAdapter,
                                 wgpu::DeviceDescriptor descriptor) override {
@@ -874,9 +875,9 @@
 class ShaderModuleExtensionValidationTestSafeAllFeatures
     : public ShaderModuleExtensionValidationTestBase {
   protected:
-    void CreateTestAdapter(wgpu::RequestAdapterOptions options) override {
+    void CreateTestAdapter(wgpu::Instance instance, wgpu::RequestAdapterOptions options) override {
         // Create a safe adapter
-        CreateTestAdapterWithUnsafeAPIToggle(options, false);
+        CreateTestAdapterWithUnsafeAPIToggle(instance, options, false);
     }
     WGPUDevice CreateTestDevice(native::Adapter dawnAdapter,
                                 wgpu::DeviceDescriptor descriptor) override {
@@ -904,9 +905,9 @@
 class ShaderModuleExtensionValidationTestUnsafeAllFeatures
     : public ShaderModuleExtensionValidationTestBase {
   protected:
-    void CreateTestAdapter(wgpu::RequestAdapterOptions options) override {
+    void CreateTestAdapter(wgpu::Instance instance, wgpu::RequestAdapterOptions options) override {
         // Create an unsafe adapter
-        CreateTestAdapterWithUnsafeAPIToggle(options, true);
+        CreateTestAdapterWithUnsafeAPIToggle(instance, options, true);
     }
     WGPUDevice CreateTestDevice(native::Adapter dawnAdapter,
                                 wgpu::DeviceDescriptor descriptor) override {
diff --git a/src/dawn/tests/unittests/validation/ValidationTest.cpp b/src/dawn/tests/unittests/validation/ValidationTest.cpp
index 7afa6cb..d3d65b6 100644
--- a/src/dawn/tests/unittests/validation/ValidationTest.cpp
+++ b/src/dawn/tests/unittests/validation/ValidationTest.cpp
@@ -106,11 +106,12 @@
 
     // Forward to dawn::native instanceRequestAdapter, but save the returned adapter in
     // gCurrentTest->mBackendAdapter.
-    procs.instanceRequestAdapter = [](WGPUInstance i, const WGPURequestAdapterOptions* options,
+    procs.instanceRequestAdapter = [](WGPUInstance instance,
+                                      const WGPURequestAdapterOptions* options,
                                       WGPURequestAdapterCallback callback, void* userdata) {
         DAWN_ASSERT(gCurrentTest);
         dawn::native::GetProcs().instanceRequestAdapter(
-            i, options,
+            instance, options,
             [](WGPURequestAdapterStatus status, WGPUAdapter cAdapter, char const* message,
                void* userdata) {
                 gCurrentTest->mBackendAdapter = dawn::native::FromAPI(cAdapter);
@@ -141,11 +142,6 @@
 }
 
 void ValidationTest::SetUp() {
-    std::string traceName =
-        std::string(::testing::UnitTest::GetInstance()->current_test_info()->test_suite_name()) +
-        "_" + ::testing::UnitTest::GetInstance()->current_test_info()->name();
-    mWireHelper->BeginWireTrace(traceName.c_str());
-
     // Create an instance with toggle AllowUnsafeAPIs enabled, which would be inherited to
     // adapter and device toggles and allow us to test unsafe apis (including experimental
     // features). To test device with AllowUnsafeAPIs disabled, require it in device toggles
@@ -157,7 +153,28 @@
 
     wgpu::InstanceDescriptor instanceDesc = {};
     instanceDesc.nextInChain = &instanceToggles;
-    ReinitializeInstances(&instanceDesc);
+    std::tie(mInstance, mDawnInstance) = mWireHelper->CreateInstances(&instanceDesc);
+
+    std::string traceName =
+        std::string(::testing::UnitTest::GetInstance()->current_test_info()->test_suite_name()) +
+        "_" + ::testing::UnitTest::GetInstance()->current_test_info()->name();
+    mWireHelper->BeginWireTrace(traceName.c_str());
+
+    wgpu::RequestAdapterOptions options = {};
+    options.backendType = wgpu::BackendType::Null;
+    options.compatibilityMode = gCurrentTest->UseCompatibilityMode();
+
+    CreateTestAdapter(mInstance, options);
+    DAWN_ASSERT(adapter);
+
+    wgpu::DeviceDescriptor deviceDescriptor = {};
+    deviceDescriptor.deviceLostCallback = ValidationTest::OnDeviceLost;
+    deviceDescriptor.deviceLostUserdata = this;
+
+    device = RequestDeviceSync(deviceDescriptor);
+    backendDevice = mLastCreatedBackendDevice;
+
+    device.SetUncapturedErrorCallback(ValidationTest::OnDeviceError, this);
 }
 
 ValidationTest::~ValidationTest() {
@@ -165,7 +182,7 @@
     // dawn*Release will call a nullptr
     device = nullptr;
     adapter = nullptr;
-    instance = nullptr;
+    mInstance = nullptr;
     mWireHelper.reset();
 
     // Check that all devices were destructed.
@@ -231,7 +248,7 @@
 
     // Force the currently submitted operations to completed.
     while (!done) {
-        instance.ProcessEvents();
+        waitDevice.Tick();
         FlushWire();
     }
 
@@ -282,7 +299,8 @@
     return mBackendAdapter;
 }
 
-void ValidationTest::CreateTestAdapter(wgpu::RequestAdapterOptions options) {
+void ValidationTest::CreateTestAdapter(wgpu::Instance instance,
+                                       wgpu::RequestAdapterOptions options) {
     instance.RequestAdapter(
         &options,
         [](WGPURequestAdapterStatus, WGPUAdapter cAdapter, const char*, void* userdata) {
@@ -316,30 +334,6 @@
     return dawnAdapter.CreateDevice(&deviceDescriptor);
 }
 
-void ValidationTest::ReinitializeInstances(const wgpu::InstanceDescriptor* nativeDesc,
-                                           const wgpu::InstanceDescriptor* wireDesc) {
-    // Reinitialize the instances.
-    std::tie(instance, mDawnInstance) = mWireHelper->CreateInstances(nativeDesc, wireDesc);
-
-    // Reinitialize the adapter.
-    wgpu::RequestAdapterOptions options = {};
-    options.backendType = wgpu::BackendType::Null;
-    options.compatibilityMode = gCurrentTest->UseCompatibilityMode();
-
-    CreateTestAdapter(options);
-    DAWN_ASSERT(adapter);
-
-    // Reinitialize the device.
-    mExpectDestruction = true;
-    wgpu::DeviceDescriptor deviceDescriptor = {};
-    deviceDescriptor.deviceLostCallback = ValidationTest::OnDeviceLost;
-    deviceDescriptor.deviceLostUserdata = this;
-    device = RequestDeviceSync(deviceDescriptor);
-    backendDevice = mLastCreatedBackendDevice;
-    device.SetUncapturedErrorCallback(ValidationTest::OnDeviceError, this);
-    mExpectDestruction = false;
-}
-
 bool ValidationTest::UseCompatibilityMode() const {
     return false;
 }
diff --git a/src/dawn/tests/unittests/validation/ValidationTest.h b/src/dawn/tests/unittests/validation/ValidationTest.h
index 8708e7a..71c1f2e 100644
--- a/src/dawn/tests/unittests/validation/ValidationTest.h
+++ b/src/dawn/tests/unittests/validation/ValidationTest.h
@@ -164,19 +164,12 @@
 
   protected:
     dawn::native::Adapter& GetBackendAdapter();
-    // Helper function to create testing adapter and device during SetUp. Override these functions
-    // to change the creation behavior.
-    virtual void CreateTestAdapter(wgpu::RequestAdapterOptions options);
+    // Helper function to create testing adapter and store into ValidationTest::adapter during
+    // SetUp. Override this function to change the adapter creation behavior.
+    virtual void CreateTestAdapter(wgpu::Instance instance, wgpu::RequestAdapterOptions options);
     virtual WGPUDevice CreateTestDevice(dawn::native::Adapter dawnAdapter,
                                         wgpu::DeviceDescriptor descriptor);
 
-    // Reinitializes the ValidationTest internal members given the instance descriptors.
-    // Particularly useful when writing tests that may need to pass different toggles to the
-    // instance. Note that this also reinitializes the adapter and device on the new instances via
-    // potentially overriden CreateTest[Adapter|Device] functions above.
-    void ReinitializeInstances(const wgpu::InstanceDescriptor* nativeDesc,
-                               const wgpu::InstanceDescriptor* wireDesc = nullptr);
-
     wgpu::Device RequestDeviceSync(const wgpu::DeviceDescriptor& deviceDesc);
     static void OnDeviceError(WGPUErrorType type, const char* message, void* userdata);
     static void OnDeviceLost(WGPUDeviceLostReason reason, const char* message, void* userdata);
@@ -186,12 +179,12 @@
     wgpu::Device device;
     wgpu::Adapter adapter;
     WGPUDevice backendDevice;
-    wgpu::Instance instance;
 
     size_t mLastWarningCount = 0;
 
   private:
     std::unique_ptr<dawn::native::Instance> mDawnInstance;
+    wgpu::Instance mInstance;
     dawn::native::Adapter mBackendAdapter;
     std::unique_ptr<dawn::utils::WireHelper> mWireHelper;
     WGPUDevice mLastCreatedBackendDevice;
diff --git a/src/dawn/tests/unittests/validation/WGSLFeatureValidationTests.cpp b/src/dawn/tests/unittests/validation/WGSLFeatureValidationTests.cpp
index 526f872..61d79a6 100644
--- a/src/dawn/tests/unittests/validation/WGSLFeatureValidationTests.cpp
+++ b/src/dawn/tests/unittests/validation/WGSLFeatureValidationTests.cpp
@@ -46,7 +46,7 @@
         std::vector<const char*> blocklist = {};
     };
 
-    void ReinitializeInstances(InstanceSpec spec) {
+    wgpu::Instance CreateInstance(InstanceSpec spec) {
         // The blocklist that will be shared between both the native and wire descriptors.
         wgpu::DawnWGSLBlocklist blocklist;
         blocklist.blocklistedFeatureCount = spec.blocklist.size();
@@ -82,7 +82,45 @@
         wgpu::InstanceDescriptor wireDesc;
         wireDesc.nextInChain = &wgslControl;
 
-        ValidationTest::ReinitializeInstances(&nativeDesc, &wireDesc);
+        return GetWireHelper()->CreateInstances(&nativeDesc, &wireDesc).first;
+    }
+
+    wgpu::Device CreateDeviceOnInstance(wgpu::Instance instance) {
+        // Get the adapter
+        wgpu::Adapter adapter;
+        instance.RequestAdapter(
+            nullptr,
+            [](WGPURequestAdapterStatus status, WGPUAdapter a, const char* message,
+               void* userdata) {
+                ASSERT_EQ(status, WGPURequestAdapterStatus_Success);
+                ASSERT_NE(a, nullptr);
+                *reinterpret_cast<wgpu::Adapter*>(userdata) = wgpu::Adapter::Acquire(a);
+            },
+            &adapter);
+
+        while (!adapter) {
+            FlushWire();
+        }
+        EXPECT_NE(nullptr, adapter.Get());
+
+        // Get the device
+        wgpu::Device device;
+        adapter.RequestDevice(
+            nullptr,
+            [](WGPURequestDeviceStatus status, WGPUDevice d, const char* message, void* userdata) {
+                ASSERT_EQ(status, WGPURequestDeviceStatus_Success);
+                ASSERT_NE(d, nullptr);
+                *reinterpret_cast<wgpu::Device*>(userdata) = wgpu::Device::Acquire(d);
+            },
+            &device);
+
+        while (!device) {
+            FlushWire();
+        }
+        EXPECT_NE(nullptr, device.Get());
+
+        device.SetUncapturedErrorCallback(ValidationTest::OnDeviceError, this);
+        return device;
     }
 };
 
@@ -90,7 +128,7 @@
 
 // Check HasFeature for an Instance that doesn't have unsafe APIs.
 TEST_F(WGSLFeatureValidationTest, HasFeatureDefaultInstance) {
-    ReinitializeInstances({});
+    wgpu::Instance instance = CreateInstance({});
 
     // Shipped features are present.
     ASSERT_TRUE(instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingShipped));
@@ -111,7 +149,7 @@
 
 // Check HasFeature for an Instance that has unsafe APIs.
 TEST_F(WGSLFeatureValidationTest, HasFeatureExposeExperimental) {
-    ReinitializeInstances({.exposeExperimental = true});
+    wgpu::Instance instance = CreateInstance({.exposeExperimental = true});
 
     // Shipped and experimental features are present.
     ASSERT_TRUE(instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingShipped));
@@ -132,7 +170,7 @@
 
 // Check HasFeature for an Instance that has unsafe APIs.
 TEST_F(WGSLFeatureValidationTest, HasFeatureAllowUnsafeInstance) {
-    ReinitializeInstances({.allowUnsafeAPIs = true});
+    wgpu::Instance instance = CreateInstance({.allowUnsafeAPIs = true});
 
     // Shipped and experimental features are present.
     ASSERT_TRUE(instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingShipped));
@@ -153,7 +191,7 @@
 
 // Check HasFeature for an Instance that doesn't have the expose_wgsl_testing_features toggle.
 TEST_F(WGSLFeatureValidationTest, HasFeatureWithoutExposeWGSLTestingFeatures) {
-    ReinitializeInstances({.useTestingFeatures = false});
+    wgpu::Instance instance = CreateInstance({.useTestingFeatures = false});
 
     // None of the testing features are present.
     ASSERT_FALSE(instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingShipped));
@@ -169,7 +207,7 @@
 
 // Tests for the behavior of WGSL feature enumeration.
 TEST_F(WGSLFeatureValidationTest, EnumerateFeatures) {
-    ReinitializeInstances({});
+    wgpu::Instance instance = CreateInstance({});
 
     size_t featureCount = instance.EnumerateWGSLLanguageFeatures(nullptr);
 
@@ -205,7 +243,8 @@
 
 // Check that the enabled / disabled features are used to validate the WGSL shaders.
 TEST_F(WGSLFeatureValidationTest, UsingFeatureInShaderModule) {
-    ReinitializeInstances({});
+    wgpu::Instance instance = CreateInstance({});
+    wgpu::Device device = CreateDeviceOnInstance(instance);
 
     utils::CreateShaderModule(device, R"(
         requires chromium_testing_shipped;
@@ -227,7 +266,7 @@
 
 // Test using DawnWGSLBlocklist to block features with a killswitch by name.
 TEST_F(WGSLFeatureValidationTest, BlockListOfKillswitchedFeatures) {
-    ReinitializeInstances(
+    wgpu::Instance instance = CreateInstance(
         {.allowUnsafeAPIs = true, .blocklist = {"chromium_testing_shipped_with_killswitch"}});
 
     // The blocklisted feature is not present.
@@ -242,6 +281,7 @@
         instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingUnsafeExperimental));
 
     // Using the blocklisted extension fails.
+    wgpu::Device device = CreateDeviceOnInstance(instance);
     ASSERT_DEVICE_ERROR(utils::CreateShaderModule(device, R"(
         requires chromium_testing_shipped_with_killswitch;
     )"));
@@ -249,10 +289,10 @@
 
 // Test that DawnWGSLBlocklist can block any feature name (even without a killswitch).
 TEST_F(WGSLFeatureValidationTest, BlockListOfAnyFeature) {
-    ReinitializeInstances(
-        {.allowUnsafeAPIs = true,
-         .blocklist = {"chromium_testing_shipped", "chromium_testing_experimental",
-                       "chromium_testing_unsafe_experimental"}});
+    wgpu::Instance instance =
+        CreateInstance({.allowUnsafeAPIs = true,
+                        .blocklist = {"chromium_testing_shipped", "chromium_testing_experimental",
+                                      "chromium_testing_unsafe_experimental"}});
 
     // All blocklisted features aren't present.
     ASSERT_FALSE(instance.HasWGSLLanguageFeature(wgpu::WGSLFeatureName::ChromiumTestingShipped));
@@ -264,7 +304,7 @@
 
 // Test that DawnWGSLBlocklist can contain garbage names without causing problems.
 TEST_F(WGSLFeatureValidationTest, BlockListGarbageName) {
-    ReinitializeInstances({.blocklist = {"LE_GARBAGE"}});
+    wgpu::Instance instance = CreateInstance({.blocklist = {"LE_GARBAGE"}});
     ASSERT_NE(instance, nullptr);
 }
 
diff --git a/src/dawn/tests/unittests/wire/WireErrorCallbackTests.cpp b/src/dawn/tests/unittests/wire/WireErrorCallbackTests.cpp
index 31ad03c..4bb2abd 100644
--- a/src/dawn/tests/unittests/wire/WireErrorCallbackTests.cpp
+++ b/src/dawn/tests/unittests/wire/WireErrorCallbackTests.cpp
@@ -26,9 +26,7 @@
 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 #include <memory>
-#include <utility>
 
-#include "dawn/tests/unittests/wire/WireFutureTest.h"
 #include "dawn/tests/unittests/wire/WireTest.h"
 #include "dawn/wire/WireClient.h"
 
@@ -37,7 +35,6 @@
 
 using testing::_;
 using testing::DoAll;
-using testing::InvokeWithoutArgs;
 using testing::Mock;
 using testing::Return;
 using testing::SaveArg;
@@ -55,6 +52,16 @@
     mockDeviceErrorCallback->Call(type, message, userdata);
 }
 
+class MockDevicePopErrorScopeCallback {
+  public:
+    MOCK_METHOD(void, Call, (WGPUErrorType type, const char* message, void* userdata));
+};
+
+std::unique_ptr<StrictMock<MockDevicePopErrorScopeCallback>> mockDevicePopErrorScopeCallback;
+void ToMockDevicePopErrorScopeCallback(WGPUErrorType type, const char* message, void* userdata) {
+    mockDevicePopErrorScopeCallback->Call(type, message, userdata);
+}
+
 class MockDeviceLoggingCallback {
   public:
     MOCK_METHOD(void, Call, (WGPULoggingType type, const char* message, void* userdata));
@@ -85,6 +92,8 @@
 
         mockDeviceErrorCallback = std::make_unique<StrictMock<MockDeviceErrorCallback>>();
         mockDeviceLoggingCallback = std::make_unique<StrictMock<MockDeviceLoggingCallback>>();
+        mockDevicePopErrorScopeCallback =
+            std::make_unique<StrictMock<MockDevicePopErrorScopeCallback>>();
         mockDeviceLostCallback = std::make_unique<StrictMock<MockDeviceLostCallback>>();
     }
 
@@ -93,6 +102,7 @@
 
         mockDeviceErrorCallback = nullptr;
         mockDeviceLoggingCallback = nullptr;
+        mockDevicePopErrorScopeCallback = nullptr;
         mockDeviceLostCallback = nullptr;
     }
 
@@ -100,6 +110,7 @@
         WireTest::FlushServer();
 
         Mock::VerifyAndClearExpectations(&mockDeviceErrorCallback);
+        Mock::VerifyAndClearExpectations(&mockDevicePopErrorScopeCallback);
     }
 };
 
@@ -177,6 +188,195 @@
     FlushServer();
 }
 
+// Test the return wire for validation error scopes.
+TEST_F(WireErrorCallbackTests, PushPopValidationErrorScopeCallback) {
+    EXPECT_CALL(api, DevicePushErrorScope(apiDevice, WGPUErrorFilter_Validation)).Times(1);
+    wgpuDevicePushErrorScope(device, WGPUErrorFilter_Validation);
+    FlushClient();
+
+    WGPUErrorCallback callback;
+    void* userdata;
+    EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _))
+        .WillOnce(DoAll(SaveArg<1>(&callback), SaveArg<2>(&userdata)));
+    wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this);
+    FlushClient();
+
+    EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                Call(WGPUErrorType_Validation, StrEq("Some error message"), this))
+        .Times(1);
+    callback(WGPUErrorType_Validation, "Some error message", userdata);
+    FlushServer();
+}
+
+// Test the return wire for OOM error scopes.
+TEST_F(WireErrorCallbackTests, PushPopOOMErrorScopeCallback) {
+    EXPECT_CALL(api, DevicePushErrorScope(apiDevice, WGPUErrorFilter_OutOfMemory)).Times(1);
+    wgpuDevicePushErrorScope(device, WGPUErrorFilter_OutOfMemory);
+    FlushClient();
+
+    WGPUErrorCallback callback;
+    void* userdata;
+    EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _))
+        .WillOnce(DoAll(SaveArg<1>(&callback), SaveArg<2>(&userdata)));
+    wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this);
+    FlushClient();
+
+    EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                Call(WGPUErrorType_OutOfMemory, StrEq("Some error message"), this))
+        .Times(1);
+    callback(WGPUErrorType_OutOfMemory, "Some error message", userdata);
+    FlushServer();
+}
+
+// Test the return wire for internal error scopes.
+TEST_F(WireErrorCallbackTests, PushPopInternalErrorScopeCallback) {
+    EXPECT_CALL(api, DevicePushErrorScope(apiDevice, WGPUErrorFilter_Internal)).Times(1);
+    wgpuDevicePushErrorScope(device, WGPUErrorFilter_Internal);
+    FlushClient();
+
+    WGPUErrorCallback callback;
+    void* userdata;
+    EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _))
+        .WillOnce(DoAll(SaveArg<1>(&callback), SaveArg<2>(&userdata)));
+    wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this);
+    FlushClient();
+
+    EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                Call(WGPUErrorType_Internal, StrEq("Some error message"), this))
+        .Times(1);
+    callback(WGPUErrorType_Internal, "Some error message", userdata);
+    FlushServer();
+}
+
+// Test the return wire for error scopes when callbacks return in a various orders.
+TEST_F(WireErrorCallbackTests, PopErrorScopeCallbackOrdering) {
+    // Two error scopes are popped, and the first one returns first.
+    {
+        EXPECT_CALL(api, DevicePushErrorScope(apiDevice, WGPUErrorFilter_Validation)).Times(2);
+        wgpuDevicePushErrorScope(device, WGPUErrorFilter_Validation);
+        wgpuDevicePushErrorScope(device, WGPUErrorFilter_Validation);
+        FlushClient();
+
+        WGPUErrorCallback callback1;
+        WGPUErrorCallback callback2;
+        void* userdata1;
+        void* userdata2;
+        EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _))
+            .WillOnce(DoAll(SaveArg<1>(&callback1), SaveArg<2>(&userdata1)))
+            .WillOnce(DoAll(SaveArg<1>(&callback2), SaveArg<2>(&userdata2)));
+        wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this);
+        wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this + 1);
+        FlushClient();
+
+        EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                    Call(WGPUErrorType_Validation, StrEq("First error message"), this))
+            .Times(1);
+        callback1(WGPUErrorType_Validation, "First error message", userdata1);
+        FlushServer();
+
+        EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                    Call(WGPUErrorType_Validation, StrEq("Second error message"), this + 1))
+            .Times(1);
+        callback2(WGPUErrorType_Validation, "Second error message", userdata2);
+        FlushServer();
+    }
+
+    // Two error scopes are popped, and the second one returns first.
+    {
+        EXPECT_CALL(api, DevicePushErrorScope(apiDevice, WGPUErrorFilter_Validation)).Times(2);
+        wgpuDevicePushErrorScope(device, WGPUErrorFilter_Validation);
+        wgpuDevicePushErrorScope(device, WGPUErrorFilter_Validation);
+        FlushClient();
+
+        WGPUErrorCallback callback1;
+        WGPUErrorCallback callback2;
+        void* userdata1;
+        void* userdata2;
+        EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _))
+            .WillOnce(DoAll(SaveArg<1>(&callback1), SaveArg<2>(&userdata1)))
+            .WillOnce(DoAll(SaveArg<1>(&callback2), SaveArg<2>(&userdata2)));
+        wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this);
+        wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this + 1);
+        FlushClient();
+
+        EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                    Call(WGPUErrorType_Validation, StrEq("Second error message"), this + 1))
+            .Times(1);
+        callback2(WGPUErrorType_Validation, "Second error message", userdata2);
+        FlushServer();
+
+        EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                    Call(WGPUErrorType_Validation, StrEq("First error message"), this))
+            .Times(1);
+        callback1(WGPUErrorType_Validation, "First error message", userdata1);
+        FlushServer();
+    }
+}
+
+// Test the return wire for error scopes in flight when the device is destroyed.
+TEST_F(WireErrorCallbackTests, PopErrorScopeDeviceInFlightDestroy) {
+    EXPECT_CALL(api, DevicePushErrorScope(apiDevice, WGPUErrorFilter_Validation)).Times(1);
+    wgpuDevicePushErrorScope(device, WGPUErrorFilter_Validation);
+    FlushClient();
+
+    EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _)).Times(1);
+    wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this);
+    FlushClient();
+
+    // Incomplete callback called in Device destructor. This is resolved after the end of this
+    // test.
+    EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                Call(WGPUErrorType_Unknown, ValidStringMessage(), this))
+        .Times(1);
+}
+
+// Test that registering a callback then wire disconnect calls the callback with
+// DeviceLost.
+TEST_F(WireErrorCallbackTests, PopErrorScopeThenDisconnect) {
+    EXPECT_CALL(api, DevicePushErrorScope(apiDevice, WGPUErrorFilter_Validation)).Times(1);
+    wgpuDevicePushErrorScope(device, WGPUErrorFilter_Validation);
+
+    EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _)).Times(1);
+    wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this);
+    FlushClient();
+
+    EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                Call(WGPUErrorType_DeviceLost, ValidStringMessage(), this))
+        .Times(1);
+    GetWireClient()->Disconnect();
+}
+
+// Test that registering a callback after wire disconnect calls the callback with
+// DeviceLost.
+TEST_F(WireErrorCallbackTests, PopErrorScopeAfterDisconnect) {
+    EXPECT_CALL(api, DevicePushErrorScope(apiDevice, WGPUErrorFilter_Validation)).Times(1);
+    wgpuDevicePushErrorScope(device, WGPUErrorFilter_Validation);
+    FlushClient();
+
+    GetWireClient()->Disconnect();
+
+    EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                Call(WGPUErrorType_DeviceLost, ValidStringMessage(), this))
+        .Times(1);
+    wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this);
+}
+
+// Empty stack (We are emulating the errors that would be callback-ed from native).
+TEST_F(WireErrorCallbackTests, PopErrorScopeEmptyStack) {
+    WGPUErrorCallback callback;
+    void* userdata;
+    EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _))
+        .WillOnce(DoAll(SaveArg<1>(&callback), SaveArg<2>(&userdata)));
+    wgpuDevicePopErrorScope(device, ToMockDevicePopErrorScopeCallback, this);
+    FlushClient();
+
+    EXPECT_CALL(*mockDevicePopErrorScopeCallback,
+                Call(WGPUErrorType_Validation, StrEq("No error scopes to pop"), this))
+        .Times(1);
+    callback(WGPUErrorType_Validation, "No error scopes to pop", userdata);
+    FlushServer();
+}
+
 // Test the return wire for device lost callback
 TEST_F(WireErrorCallbackTests, DeviceLostCallback) {
     wgpuDeviceSetDeviceLostCallback(device, ToMockDeviceLostCallback, this);
@@ -196,161 +396,5 @@
     FlushServer();
 }
 
-class WirePopErrorScopeCallbackTests : public WireFutureTestWithParamsBase<> {
-  protected:
-    // Overridden version of wgpuDevicePopErrorScope that defers to the API call based on the test
-    // callback mode.
-    void DevicePopErrorScope(WGPUDevice d, void* userdata = nullptr) {
-        if (this->IsAsync()) {
-            wgpuDevicePopErrorScope(d, mMockOldCb.Callback(), mMockOldCb.MakeUserdata(userdata));
-        } else {
-            WGPUPopErrorScopeCallbackInfo callbackInfo = {};
-            callbackInfo.mode = ToWGPUCallbackMode(GetParam().mCallbackMode);
-            callbackInfo.callback = mMockCb.Callback();
-            callbackInfo.userdata = mMockCb.MakeUserdata(userdata);
-            this->mFutureIDs.push_back(wgpuDevicePopErrorScopeF(d, callbackInfo).id);
-        }
-    }
-
-    void PushErrorScope(WGPUErrorFilter filter) {
-        EXPECT_CALL(api, DevicePushErrorScope(apiDevice, filter)).Times(1);
-        wgpuDevicePushErrorScope(device, filter);
-        FlushClient();
-    }
-
-    void ExpectWireCallbacksWhen(
-        std::function<void(testing::MockCallback<WGPUErrorCallback>&)> oldExp,
-        std::function<void(testing::MockCallback<WGPUPopErrorScopeCallback>&)> exp) {
-        if (IsAsync()) {
-            oldExp(mMockOldCb);
-            ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&mMockOldCb));
-        } else {
-            exp(mMockCb);
-            ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&mMockCb));
-        }
-    }
-
-  private:
-    testing::MockCallback<WGPUPopErrorScopeCallback> mMockCb;
-    testing::MockCallback<WGPUErrorCallback> mMockOldCb;
-};
-DAWN_INSTANTIATE_WIRE_FUTURE_TEST_P(WirePopErrorScopeCallbackTests);
-
-// Test the return wire for validation error scopes.
-TEST_P(WirePopErrorScopeCallbackTests, TypeAndFilters) {
-    static constexpr std::array<std::pair<WGPUErrorType, WGPUErrorFilter>, 3> kErrorTypeAndFilters =
-        {{{WGPUErrorType_Validation, WGPUErrorFilter_Validation},
-          {WGPUErrorType_OutOfMemory, WGPUErrorFilter_OutOfMemory},
-          {WGPUErrorType_Internal, WGPUErrorFilter_Internal}}};
-
-    for (const auto& [type, filter] : kErrorTypeAndFilters) {
-        PushErrorScope(filter);
-
-        DevicePopErrorScope(device, this);
-        EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _)).WillOnce(InvokeWithoutArgs([&] {
-            api.CallDevicePopErrorScopeCallback(apiDevice, type, "Some error message");
-        }));
-
-        FlushClient();
-        FlushFutures();
-        ExpectWireCallbacksWhen(
-            [&](auto& oldMockCb) {
-                EXPECT_CALL(oldMockCb, Call(type, StrEq("Some error message"), this)).Times(1);
-
-                FlushCallbacks();
-            },
-            [&](auto& mockCb) {
-                EXPECT_CALL(mockCb, Call(WGPUPopErrorScopeStatus_Success, type,
-                                         StrEq("Some error message"), this))
-                    .Times(1);
-
-                FlushCallbacks();
-            });
-    }
-}
-
-// Wire disconnect before server response calls the callback with Unknown error type.
-// TODO(crbug.com/dawn/2021) When using new callback signature, check for InstanceDropped status.
-TEST_P(WirePopErrorScopeCallbackTests, DisconnectBeforeServerReply) {
-    PushErrorScope(WGPUErrorFilter_Validation);
-
-    DevicePopErrorScope(device, this);
-    EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _)).Times(1);
-
-    FlushClient();
-    FlushFutures();
-    ExpectWireCallbacksWhen(
-        [&](auto& oldMockCb) {
-            EXPECT_CALL(oldMockCb, Call(WGPUErrorType_Unknown, nullptr, this)).Times(1);
-
-            GetWireClient()->Disconnect();
-        },
-        [&](auto& mockCb) {
-            EXPECT_CALL(mockCb, Call(WGPUPopErrorScopeStatus_InstanceDropped, WGPUErrorType_Unknown,
-                                     nullptr, this))
-                .Times(1);
-
-            GetWireClient()->Disconnect();
-        });
-}
-
-// Wire disconnect after server response calls the callback with returned error type.
-TEST_P(WirePopErrorScopeCallbackTests, DisconnectAfterServerReply) {
-    // On Async and Spontaneous mode, it is not possible to simulate this because on the server
-    // reponse, the callback would also be fired.
-    DAWN_SKIP_TEST_IF(IsSpontaneous());
-
-    PushErrorScope(WGPUErrorFilter_Validation);
-
-    DevicePopErrorScope(device, this);
-    EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _)).WillOnce(InvokeWithoutArgs([&] {
-        api.CallDevicePopErrorScopeCallback(apiDevice, WGPUErrorType_Validation,
-                                            "Some error message");
-    }));
-
-    FlushClient();
-    FlushFutures();
-    ExpectWireCallbacksWhen(
-        [&](auto& oldMockCb) {
-            EXPECT_CALL(oldMockCb, Call(WGPUErrorType_Validation, nullptr, this)).Times(1);
-
-            GetWireClient()->Disconnect();
-        },
-        [&](auto& mockCb) {
-            EXPECT_CALL(mockCb, Call(WGPUPopErrorScopeStatus_InstanceDropped,
-                                     WGPUErrorType_Validation, nullptr, this))
-                .Times(1);
-
-            GetWireClient()->Disconnect();
-        });
-}
-
-// Empty stack (We are emulating the errors that would be callback-ed from native).
-TEST_P(WirePopErrorScopeCallbackTests, EmptyStack) {
-    DevicePopErrorScope(device, this);
-    EXPECT_CALL(api, OnDevicePopErrorScope(apiDevice, _, _)).WillOnce(InvokeWithoutArgs([&] {
-        api.CallDevicePopErrorScopeCallback(apiDevice, WGPUErrorType_Validation,
-                                            "No error scopes to pop");
-    }));
-
-    FlushClient();
-    FlushFutures();
-    ExpectWireCallbacksWhen(
-        [&](auto& oldMockCb) {
-            EXPECT_CALL(oldMockCb,
-                        Call(WGPUErrorType_Validation, StrEq("No error scopes to pop"), this))
-                .Times(1);
-
-            FlushCallbacks();
-        },
-        [&](auto& mockCb) {
-            EXPECT_CALL(mockCb, Call(WGPUPopErrorScopeStatus_Success, WGPUErrorType_Validation,
-                                     StrEq("No error scopes to pop"), this))
-                .Times(1);
-
-            FlushCallbacks();
-        });
-}
-
 }  // anonymous namespace
 }  // namespace dawn::wire
diff --git a/src/dawn/tests/unittests/wire/WireFutureTest.h b/src/dawn/tests/unittests/wire/WireFutureTest.h
index 5b26c93..042db62 100644
--- a/src/dawn/tests/unittests/wire/WireFutureTest.h
+++ b/src/dawn/tests/unittests/wire/WireFutureTest.h
@@ -85,11 +85,34 @@
         , testName, testing::ValuesIn(MakeParamGenerator<testName::ParamType>(__VA_ARGS__)), \
         &TestParamToString<testName::ParamType>)
 
-template <typename Params = WireFutureTestParam>
-class WireFutureTestWithParamsBase : public WireTest, public testing::WithParamInterface<Params> {
+template <typename Callback,
+          typename CallbackInfo,
+          auto& AsyncF,
+          auto& FutureF,
+          typename Params = WireFutureTestParam,
+          typename AsyncFT = decltype(AsyncF),
+          typename FutureFT = decltype(FutureF)>
+class WireFutureTestWithParams : public WireTest, public testing::WithParamInterface<Params> {
   protected:
     using testing::WithParamInterface<Params>::GetParam;
 
+    // Calls the actual API that the test suite is exercising given the callback mode. This should
+    // be used in favor of directly calling the API because the Async mode actually calls a
+    // different entry point.
+    template <typename... Args>
+    void CallImpl(void* userdata, Args&&... args) {
+        if (GetParam().mCallbackMode == CallbackMode::Async) {
+            mAsyncF(std::forward<Args>(args)..., mMockCb.Callback(),
+                    mMockCb.MakeUserdata(userdata));
+        } else {
+            CallbackInfo callbackInfo = {};
+            callbackInfo.mode = ToWGPUCallbackMode(GetParam().mCallbackMode);
+            callbackInfo.callback = mMockCb.Callback();
+            callbackInfo.userdata = mMockCb.MakeUserdata(userdata);
+            mFutureIDs.push_back(mFutureF(std::forward<Args>(args)..., callbackInfo).id);
+        }
+    }
+
     // Events are considered spontaneous if either we are using the legacy Async or the new
     // Spontaneous modes.
     bool IsSpontaneous() {
@@ -97,8 +120,24 @@
         return callbackMode == CallbackMode::Async || callbackMode == CallbackMode::Spontaneous;
     }
 
-    // Returns true iff testing older Async entry points, i.e. not Future related entry points.
-    bool IsAsync() { return GetParam().mCallbackMode == CallbackMode::Async; }
+    // In order to tightly bound when callbacks are expected to occur, test writers only have access
+    // to the mock callback via the argument passed usually via a lamdba. The 'exp' lambda should
+    // generally be a block of expectations on the mock callback followed by one statement where we
+    // expect the callbacks to be called from. If the callbacks do not occur in the scope of the
+    // lambda, the mock will fail the test.
+    //
+    // Usage:
+    //   ExpectWireCallbackWhen([&](auto& mockCb) {
+    //       // Set scoped expectations on the mock callback.
+    //       EXPECT_CALL(mockCb, Call).Times(1);
+    //
+    //       // Call the statement where we want to ensure the callbacks occur.
+    //       FlushCallbacks();
+    //   });
+    void ExpectWireCallbacksWhen(std::function<void(testing::MockCallback<Callback>&)> exp) {
+        exp(mMockCb);
+        ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&mMockCb));
+    }
 
     // Future suite adds the following flush mechanics for test writers so that they can have fine
     // grained control over when expectations should be set and verified.
@@ -150,57 +189,11 @@
         }
     }
 
-    std::vector<FutureID> mFutureIDs;
-};
-
-template <typename Callback,
-          typename CallbackInfo,
-          auto& AsyncF,
-          auto& FutureF,
-          typename Params = WireFutureTestParam,
-          typename AsyncFT = decltype(AsyncF),
-          typename FutureFT = decltype(FutureF)>
-class WireFutureTestWithParams : public WireFutureTestWithParamsBase<Params> {
-  protected:
-    // Calls the actual API that the test suite is exercising given the callback mode. This should
-    // be used in favor of directly calling the API because the Async mode actually calls a
-    // different entry point.
-    template <typename... Args>
-    void CallImpl(void* userdata, Args&&... args) {
-        if (this->IsAsync()) {
-            mAsyncF(std::forward<Args>(args)..., mMockCb.Callback(),
-                    mMockCb.MakeUserdata(userdata));
-        } else {
-            CallbackInfo callbackInfo = {};
-            callbackInfo.mode = ToWGPUCallbackMode(this->GetParam().mCallbackMode);
-            callbackInfo.callback = mMockCb.Callback();
-            callbackInfo.userdata = mMockCb.MakeUserdata(userdata);
-            this->mFutureIDs.push_back(mFutureF(std::forward<Args>(args)..., callbackInfo).id);
-        }
-    }
-
-    // In order to tightly bound when callbacks are expected to occur, test writers only have access
-    // to the mock callback via the argument passed usually via a lamdba. The 'exp' lambda should
-    // generally be a block of expectations on the mock callback followed by one statement where we
-    // expect the callbacks to be called from. If the callbacks do not occur in the scope of the
-    // lambda, the mock will fail the test.
-    //
-    // Usage:
-    //   ExpectWireCallbackWhen([&](auto& mockCb) {
-    //       // Set scoped expectations on the mock callback.
-    //       EXPECT_CALL(mockCb, Call).Times(1);
-    //
-    //       // Call the statement where we want to ensure the callbacks occur.
-    //       FlushCallbacks();
-    //   });
-    void ExpectWireCallbacksWhen(std::function<void(testing::MockCallback<Callback>&)> exp) {
-        exp(mMockCb);
-        ASSERT_TRUE(testing::Mock::VerifyAndClearExpectations(&mMockCb));
-    }
-
   private:
     AsyncFT mAsyncF = AsyncF;
     FutureFT mFutureF = FutureF;
+    std::vector<FutureID> mFutureIDs;
+
     testing::MockCallback<Callback> mMockCb;
 };
 
diff --git a/src/dawn/wire/client/ClientDoers.cpp b/src/dawn/wire/client/ClientDoers.cpp
index ba4064d..4f0b053 100644
--- a/src/dawn/wire/client/ClientDoers.cpp
+++ b/src/dawn/wire/client/ClientDoers.cpp
@@ -77,6 +77,20 @@
     return WireResult::Success;
 }
 
+WireResult Client::DoDevicePopErrorScopeCallback(Device* device,
+                                                 uint64_t requestSerial,
+                                                 WGPUErrorType errorType,
+                                                 const char* message) {
+    if (device == nullptr) {
+        // The device might have been deleted or recreated so this isn't an error.
+        return WireResult::Success;
+    }
+    if (device->OnPopErrorScopeCallback(requestSerial, errorType, message)) {
+        return WireResult::Success;
+    }
+    return WireResult::FatalError;
+}
+
 WireResult Client::DoShaderModuleGetCompilationInfoCallback(ShaderModule* shaderModule,
                                                             uint64_t requestSerial,
                                                             WGPUCompilationInfoRequestStatus status,
diff --git a/src/dawn/wire/client/Device.cpp b/src/dawn/wire/client/Device.cpp
index d7d2620..68cf129 100644
--- a/src/dawn/wire/client/Device.cpp
+++ b/src/dawn/wire/client/Device.cpp
@@ -41,55 +41,6 @@
 namespace dawn::wire::client {
 namespace {
 
-class PopErrorScopeEvent final : public TrackedEvent {
-  public:
-    static constexpr EventType kType = EventType::PopErrorScope;
-
-    explicit PopErrorScopeEvent(const WGPUPopErrorScopeCallbackInfo& callbackInfo)
-        : TrackedEvent(callbackInfo.mode),
-          mCallback(callbackInfo.callback),
-          mOldCallback(callbackInfo.oldCallback),
-          mUserdata(callbackInfo.userdata) {
-        // Exactly 1 callback should be set.
-        DAWN_ASSERT((mCallback != nullptr && mOldCallback == nullptr) ||
-                    (mCallback == nullptr && mOldCallback != nullptr));
-    }
-
-    EventType GetType() override { return kType; }
-
-    WireResult ReadyHook(FutureID futureID, WGPUErrorType errorType, const char* message) {
-        mType = errorType;
-        if (message != nullptr) {
-            mMessage = message;
-        }
-        return WireResult::Success;
-    }
-
-  private:
-    void CompleteImpl(FutureID futureID, EventCompletionType completionType) override {
-        if (completionType == EventCompletionType::Shutdown) {
-            mStatus = WGPUPopErrorScopeStatus_InstanceDropped;
-            mMessage = std::nullopt;
-        }
-        if (mOldCallback) {
-            mOldCallback(mType, mMessage ? mMessage->c_str() : nullptr, mUserdata);
-        }
-        if (mCallback) {
-            mCallback(mStatus, mType, mMessage ? mMessage->c_str() : nullptr, mUserdata);
-        }
-    }
-
-    // TODO(crbug.com/dawn/2021) Remove the old callback type.
-    WGPUPopErrorScopeCallback mCallback;
-    WGPUErrorCallback mOldCallback;
-    // TODO(https://crbug.com/dawn/2345): Investigate `DanglingUntriaged` in dawn/wire.
-    raw_ptr<void, DanglingUntriaged> mUserdata;
-
-    WGPUPopErrorScopeStatus mStatus = WGPUPopErrorScopeStatus_Success;
-    WGPUErrorType mType = WGPUErrorType_Unknown;
-    std::optional<std::string> mMessage;
-};
-
 template <typename PipelineT, EventType Type, typename CallbackInfoT>
 class CreatePipelineEventBase : public TrackedEvent {
   public:
@@ -199,6 +150,11 @@
 }
 
 Device::~Device() {
+    mErrorScopes.CloseAll([](ErrorScopeData* request) {
+        request->callback(WGPUErrorType_Unknown, "Device destroyed before callback",
+                          request->userdata);
+    });
+
     if (mQueue != nullptr) {
         GetProcs().queueRelease(ToAPI(mQueue));
     }
@@ -248,6 +204,12 @@
     }
 }
 
+void Device::CancelCallbacksForDisconnect() {
+    mErrorScopes.CloseAll([](ErrorScopeData* request) {
+        request->callback(WGPUErrorType_DeviceLost, "Device lost", request->userdata);
+    });
+}
+
 std::weak_ptr<bool> Device::GetAliveWeakPtr() {
     return mIsAlive;
 }
@@ -268,35 +230,41 @@
 }
 
 void Device::PopErrorScope(WGPUErrorCallback callback, void* userdata) {
-    WGPUPopErrorScopeCallbackInfo callbackInfo = {};
-    callbackInfo.mode = WGPUCallbackMode_AllowSpontaneous;
-    callbackInfo.oldCallback = callback;
-    callbackInfo.userdata = userdata;
-    PopErrorScopeF(callbackInfo);
-}
-
-WGPUFuture Device::PopErrorScopeF(const WGPUPopErrorScopeCallbackInfo& callbackInfo) {
     Client* client = GetClient();
-    auto [futureIDInternal, tracked] =
-        GetEventManager().TrackEvent(std::make_unique<PopErrorScopeEvent>(callbackInfo));
-    if (!tracked) {
-        return {futureIDInternal};
+    if (client->IsDisconnected()) {
+        callback(WGPUErrorType_DeviceLost, "GPU device disconnected", userdata);
+        return;
     }
 
+    uint64_t serial = mErrorScopes.Add({callback, userdata});
     DevicePopErrorScopeCmd cmd;
     cmd.deviceId = GetWireId();
-    cmd.eventManagerHandle = GetEventManagerHandle();
-    cmd.future = {futureIDInternal};
+    cmd.requestSerial = serial;
     client->SerializeCommand(cmd);
-    return {futureIDInternal};
 }
 
-WireResult Client::DoDevicePopErrorScopeCallback(ObjectHandle eventManager,
-                                                 WGPUFuture future,
-                                                 WGPUErrorType errorType,
-                                                 const char* message) {
-    return GetEventManager(eventManager)
-        .SetFutureReady<PopErrorScopeEvent>(future.id, errorType, message);
+bool Device::OnPopErrorScopeCallback(uint64_t requestSerial,
+                                     WGPUErrorType type,
+                                     const char* message) {
+    switch (type) {
+        case WGPUErrorType_NoError:
+        case WGPUErrorType_Validation:
+        case WGPUErrorType_OutOfMemory:
+        case WGPUErrorType_Internal:
+        case WGPUErrorType_Unknown:
+        case WGPUErrorType_DeviceLost:
+            break;
+        default:
+            return false;
+    }
+
+    ErrorScopeData request;
+    if (!mErrorScopes.Acquire(requestSerial, &request)) {
+        return false;
+    }
+
+    request.callback(type, message, request.userdata);
+    return true;
 }
 
 void Device::InjectError(WGPUErrorType type, const char* message) {
diff --git a/src/dawn/wire/client/Device.h b/src/dawn/wire/client/Device.h
index a04baf1..d9d9170 100644
--- a/src/dawn/wire/client/Device.h
+++ b/src/dawn/wire/client/Device.h
@@ -58,7 +58,6 @@
     void SetDeviceLostCallback(WGPUDeviceLostCallback errorCallback, void* errorUserdata);
     void InjectError(WGPUErrorType type, const char* message);
     void PopErrorScope(WGPUErrorCallback callback, void* userdata);
-    WGPUFuture PopErrorScopeF(const WGPUPopErrorScopeCallbackInfo& callbackInfo);
     WGPUBuffer CreateBuffer(const WGPUBufferDescriptor* descriptor);
     void CreateComputePipelineAsync(WGPUComputePipelineDescriptor const* descriptor,
                                     WGPUCreateComputePipelineAsyncCallback callback,
@@ -76,6 +75,7 @@
     void HandleError(WGPUErrorType errorType, const char* message);
     void HandleLogging(WGPULoggingType loggingType, const char* message);
     void HandleDeviceLost(WGPUDeviceLostReason reason, const char* message);
+    bool OnPopErrorScopeCallback(uint64_t requestSerial, WGPUErrorType type, const char* message);
 
     bool GetLimits(WGPUSupportedLimits* limits) const;
     bool HasFeature(WGPUFeatureName feature) const;
@@ -85,6 +85,8 @@
 
     WGPUQueue GetQueue();
 
+    void CancelCallbacksForDisconnect() override;
+
     std::weak_ptr<bool> GetAliveWeakPtr();
 
   private:
@@ -95,6 +97,12 @@
     WGPUFuture CreatePipelineAsyncF(Descriptor const* descriptor, const CallbackInfo& callbackInfo);
 
     LimitsAndFeatures mLimitsAndFeatures;
+    struct ErrorScopeData {
+        WGPUErrorCallback callback = nullptr;
+        // TODO(https://crbug.com/dawn/2345): Investigate `DanglingUntriaged` in dawn/wire.
+        raw_ptr<void, DanglingUntriaged> userdata = nullptr;
+    };
+    RequestTracker<ErrorScopeData> mErrorScopes;
 
     WGPUErrorCallback mErrorCallback = nullptr;
     WGPUDeviceLostCallback mDeviceLostCallback = nullptr;
diff --git a/src/dawn/wire/client/EventManager.h b/src/dawn/wire/client/EventManager.h
index b3123ca..c9c1415 100644
--- a/src/dawn/wire/client/EventManager.h
+++ b/src/dawn/wire/client/EventManager.h
@@ -49,7 +49,6 @@
     CreateComputePipeline,
     CreateRenderPipeline,
     MapAsync,
-    PopErrorScope,
     RequestAdapter,
     RequestDevice,
     WorkDone,
diff --git a/src/dawn/wire/server/Server.h b/src/dawn/wire/server/Server.h
index 7c74e77..d4d0ec9 100644
--- a/src/dawn/wire/server/Server.h
+++ b/src/dawn/wire/server/Server.h
@@ -117,8 +117,7 @@
     using CallbackUserdata::CallbackUserdata;
 
     ObjectHandle device;
-    ObjectHandle eventManager;
-    WGPUFuture future;
+    uint64_t requestSerial;
 };
 
 struct ShaderModuleGetCompilationInfoUserdata : CallbackUserdata {
diff --git a/src/dawn/wire/server/ServerDevice.cpp b/src/dawn/wire/server/ServerDevice.cpp
index 3d9fd40..aa0a27b 100644
--- a/src/dawn/wire/server/ServerDevice.cpp
+++ b/src/dawn/wire/server/ServerDevice.cpp
@@ -74,13 +74,10 @@
     SerializeCommand(cmd);
 }
 
-WireResult Server::DoDevicePopErrorScope(Known<WGPUDevice> device,
-                                         ObjectHandle eventManager,
-                                         WGPUFuture future) {
+WireResult Server::DoDevicePopErrorScope(Known<WGPUDevice> device, uint64_t requestSerial) {
     auto userdata = MakeUserdata<ErrorScopeUserdata>();
+    userdata->requestSerial = requestSerial;
     userdata->device = device.AsHandle();
-    userdata->eventManager = eventManager;
-    userdata->future = future;
 
     mProcs.devicePopErrorScope(device->handle, ForwardToServer<&Server::OnDevicePopErrorScope>,
                                userdata.release());
@@ -91,8 +88,8 @@
                                    WGPUErrorType type,
                                    const char* message) {
     ReturnDevicePopErrorScopeCallbackCmd cmd;
-    cmd.eventManager = userdata->eventManager;
-    cmd.future = userdata->future;
+    cmd.device = userdata->device;
+    cmd.requestSerial = userdata->requestSerial;
     cmd.type = type;
     cmd.message = message;