Skip to content
File

Blob: src/workerd/jsg/promise.c++

7.8 KB
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5#include "async-context.h"
6#include "jsg.h"
7 
8namespace workerd::jsg {
9 
10namespace {
11template <typename T>
12v8::Local<T> getLocal(v8::Isolate* isolate, v8::Global<T>& global) {
13 if (!global.IsEmpty()) {
14 return global.Get(isolate);
15 }
16 return v8::Local<T>();
17};
18 
19kj::Maybe<Ref<AsyncContextFrame>> getFrameRef(jsg::Lock& js) {
20 return AsyncContextFrame::current(js).map(
21 [](AsyncContextFrame& frame) -> Ref<AsyncContextFrame> { return frame.addRef(); });
22}
23 
24kj::Maybe<AsyncContextFrame&> tryGetFrame(kj::Maybe<Ref<AsyncContextFrame>>& maybeFrame) {
25 return maybeFrame.map(
26 [](Ref<AsyncContextFrame>& frame) -> AsyncContextFrame& { return *frame.get(); });
27}
28} // namespace
29 
30UnhandledRejectionHandler::UnhandledRejection::UnhandledRejection(jsg::Lock& js,
31 jsg::V8Ref<v8::Promise> promise,
32 jsg::Value value,
33 v8::Local<v8::Message> message)
34 : hash(kj::hashCode(promise.getHandle(js)->GetIdentityHash())),
35 promise(js.v8Isolate, promise.getHandle(js)),
36 value(js.v8Isolate, value.getHandle(js)),
37 message(js.v8Isolate, message),
38 asyncContextFrame(getFrameRef(js)) {}
39 
40void UnhandledRejectionHandler::report(
41 Lock& js, v8::PromiseRejectEvent event, jsg::V8Ref<v8::Promise> promise, jsg::Value value) {
42 js.tryCatch([&] {
43 switch (event) {
44 case v8::PromiseRejectEvent::kPromiseRejectWithNoHandler: {
45 rejectedWithNoHandler(js, kj::mv(promise), kj::mv(value));
46 return;
47 }
48 case v8::PromiseRejectEvent::kPromiseHandlerAddedAfterReject: {
49 handledAfterRejection(js, kj::mv(promise));
50 return;
51 }
52 case v8::PromiseRejectEvent::kPromiseRejectAfterResolved: {
53 break;
54 }
55 case v8::PromiseRejectEvent::kPromiseResolveAfterResolved: {
56 break;
57 }
58 }
59 }, [&](Value exception) {
60 // Exceptions here should be rare but possible. Any errors that occur
61 // here are likely fatal to the worker. This handling helps us avoid
62 // crashing. We'll log the error hand continue.
63 if (js.areWarningsLogged()) {
64 js.logWarning(kj::str("There was an error while reporting an unhandled promise rejection: ",
65 exception.getHandle(js)));
66 }
67 });
68}
69 
70UnhandledRejectionHandler::UnhandledRejection::~UnhandledRejection() {
71 if (promise.IsWeak()) {
72 promise.ClearWeak<void>();
73 }
74 if (value.IsWeak()) {
75 value.ClearWeak<void>();
76 }
77}
78 
79void UnhandledRejectionHandler::clear() {
80 warnedRejections.clear();
81 unhandledRejections.clear();
82}
83 
84void UnhandledRejectionHandler::rejectedWithNoHandler(
85 jsg::Lock& js, jsg::V8Ref<v8::Promise> promise, jsg::V8Ref<v8::Value> value) {
86 auto message = v8::Exception::CreateMessage(js.v8Isolate, value.getHandle(js));
87 
88 // It's not yet clear under what conditions it happens, but this can be called
89 // twice with the same promise. It really shouldn't happen in the regular cases
90 // but we address the edge case by using upsert and just replacing the existing
91 // value and message when it does.
92 
93 unhandledRejections.upsert(
94 UnhandledRejection(js, kj::mv(promise), kj::mv(value), kj::mv(message)),
95 [&](UnhandledRejection& existing, UnhandledRejection&& replacement) {
96 // Replacing the promise here is defensive, since they have the same hash
97 // it *should* be the same promise, but let's be sure. We don't need to
98 // assert here because the book keeping on this is not critical.
99 existing = kj::mv(replacement);
100 });
101 
102 ensureProcessingWarnings(js);
103}
104 
105void UnhandledRejectionHandler::handledAfterRejection(
106 jsg::Lock& js, jsg::V8Ref<v8::Promise> promise) {
107 // If an unhandled rejection is found in the table, then all we need to do is erase it.
108 // If it's not found, then we'll skip on to the next step of determining if we've already
109 // emitted an unhandled rejection warning about this promise to determine if we need to
110 // emit another warning indicating that it's been handled.
111 KJ_DEFER(ensureProcessingWarnings(js));
112 
113 HashedPromise key(promise.getHandle(js));
114 
115 if (unhandledRejections.eraseMatch(key)) {
116 return;
117 }
118 
119 KJ_IF_SOME(item, warnedRejections.find(key)) {
120 auto promise = getLocal(js.v8Isolate, item.promise);
121 if (!promise.IsEmpty()) {
122 AsyncContextFrame::Scope scope(js, tryGetFrame(item.asyncContextFrame));
123 handler(js, v8::kPromiseHandlerAddedAfterReject, jsg::HashableV8Ref(js.v8Isolate, promise),
124 js.v8Ref(js.v8Undefined()));
125 }
126 warnedRejections.release(item);
127 }
128}
129 
130void UnhandledRejectionHandler::ensureProcessingWarnings(jsg::Lock& js) {
131 if (scheduled) {
132 return;
133 }
134 scheduled = true;
135 if (useMicrotasksCompletedCallback) {
136 // Schedule processing to run after the microtask checkpoint completes.
137 // This ensures that promise chains like `.then().catch()` have fully settled
138 // before we decide a rejection is unhandled. Using a microtask would race
139 // with V8's internal promise adoption microtasks and fire too early.
140 // See https://github.com/cloudflare/workerd/issues/6020
141 js.v8Isolate->AddMicrotasksCompletedCallback(
142 &UnhandledRejectionHandler::onMicrotasksCompleted, this);
143 // Ensure we get another microtask checkpoint to deliver the callback even if
144 // we're already past the current one.
145 js.requestExtraMicrotaskCheckpoint();
146 } else {
147 js.resolvedPromise().then(js, [this](jsg::Lock& js) { processWarnings(js); });
148 }
149}
150 
151void UnhandledRejectionHandler::onMicrotasksCompleted(v8::Isolate* isolate, void* data) {
152 auto* handler = static_cast<UnhandledRejectionHandler*>(data);
153 KJ_DEFER(isolate->RemoveMicrotasksCompletedCallback(
154 &UnhandledRejectionHandler::onMicrotasksCompleted, data));
155 auto& js = Lock::from(isolate);
156 KJ_TRY {
157 handler->processWarnings(js);
158 
159 // Ensure microtasks scheduled by unhandledrejection handlers run promptly.
160 js.requestExtraMicrotaskCheckpoint();
161 }
162 KJ_CATCH(exception) {
163 handler->scheduled = false;
164 KJ_LOG(ERROR, "uncaught exception while processing unhandled rejections", exception);
165 }
166}
167 
168void UnhandledRejectionHandler::processWarnings(jsg::Lock& js) {
169 scheduled = false;
170 warnedRejections.eraseAll([](auto& value) { return !value.isAlive(); });
171 
172 while (unhandledRejections.size() > 0) {
173 auto entry = unhandledRejections.release(*unhandledRejections.begin());
174 
175 if (!entry.isAlive()) {
176 continue;
177 }
178 
179 auto promise = getLocal(js.v8Isolate, entry.promise);
180 auto value = getLocal(js.v8Isolate, entry.value);
181 
182 AsyncContextFrame::Scope scope(js, tryGetFrame(entry.asyncContextFrame));
183 
184 // Most of the time it shouldn't be found but there are times where it can
185 // be duplicated -- such as when a promise gets rejected multiple times.
186 // Check quickly before inserting to avoid a crash.
187 // Keep strong refs through dispatch, then downgrade to weak to avoid leaks.
188 entry.promise.SetWeak();
189 entry.value.SetWeak();
190 warnedRejections.upsert(
191 kj::mv(entry), [](UnhandledRejection& existing, UnhandledRejection&& replacement) {
192 // We're just going to ignore if the unhandled rejection was already here.
193 });
194 
195 js.tryCatch([&] {
196 handler(js, v8::kPromiseRejectWithNoHandler, jsg::HashableV8Ref(js.v8Isolate, promise),
197 js.v8Ref(value));
198 }, [&](Value exception) {
199 // If any exceptions occur while reporting the event, we will log them
200 // but otherwise ignore them. We do not want such errors to be fatal here.
201 if (js.areWarningsLogged()) {
202 js.logWarning(
203 kj::str("Exception while logging unhandled rejection:", exception.getHandle(js)));
204 }
205 });
206 }
207}
208 
209void UnhandledRejectionHandler::UnhandledRejection::visitForMemoryInfo(
210 MemoryTracker& tracker) const {
211 tracker.trackField("asyncContextFrame", asyncContextFrame);
212}
213 
214} // namespace workerd::jsg