-
Notifications
You must be signed in to change notification settings - Fork 504
DO NOT MERGE: JSG_TRY, JSG_TRY_CATCH, KJ_TRY, KJ_CATCH proof of concept macros #5802
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
10dade7
7448a84
7ad196e
ebf1417
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -806,13 +806,17 @@ DigestStream::DigestStream(kj::Own<WritableStreamController> controller, | |
| state(Ready(kj::mv(algorithm), kj::mv(resolver))) {} | ||
|
|
||
| void DigestStream::dispose(jsg::Lock& js) { | ||
| js.tryCatch([&] { | ||
| JSG_TRY { | ||
| KJ_IF_SOME(ready, state.tryGet<Ready>()) { | ||
| auto reason = js.typeError("The DigestStream was disposed."); | ||
| ready.resolver.reject(js, reason); | ||
| state.init<StreamStates::Errored>(js.v8Ref<v8::Value>(reason)); | ||
| } | ||
| }, [&](jsg::Value exception) { js.throwException(kj::mv(exception)); }); | ||
| } | ||
| catch (...) { | ||
| jsg::Value exception = getCaughtExceptionAsJsg(); | ||
|
Comment on lines
+816
to
+817
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part is likely to be a common pattern... perhaps even something like... To boilerplate this particular piece might be handy? |
||
| js.throwException(kj::mv(exception)); | ||
| } | ||
| } | ||
|
|
||
| void DigestStream::visitForMemoryInfo(jsg::MemoryTracker& tracker) const { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,17 +42,18 @@ MessagePort::MessagePort() | |
| } | ||
|
|
||
| void MessagePort::dispatchMessage(jsg::Lock& js, const jsg::JsValue& value) { | ||
| js.tryCatch([&] { | ||
| JSG_TRY_CATCH(getMessageException) try { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An example of how A version of JSG_TRY try {
...
} catch (...) {
jsg::Value exception = getCaughtExceptionAsJsg();
} |
||
| auto message = js.alloc<MessageEvent>(js, kj::str("message"), value, kj::String(), JSG_THIS); | ||
| dispatchEventImpl(js, kj::mv(message)); | ||
| }, [&](jsg::Value exception) { | ||
| } catch (...) { | ||
| // There was an error dispatching the message event. | ||
| // We will dispatch a messageerror event instead. | ||
| jsg::Value exception = getMessageException(); | ||
| auto message = js.alloc<MessageEvent>( | ||
| js, kj::str("message"), jsg::JsValue(exception.getHandle(js)), kj::String(), JSG_THIS); | ||
| dispatchEventImpl(js, kj::mv(message)); | ||
| // Now, if this dispatchEventImpl throws, we just blow up. Don't try to catch it. | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| // Deliver the message to this port, buffering if necessary if the port | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -505,31 +505,46 @@ jsg::Promise<void> maybeRunAlgorithm( | |
| // throws synchronously, we have to convert that synchronous throw | ||
| // into a proper rejected jsg::Promise. | ||
| KJ_IF_SOME(algorithm, maybeAlgorithm) { | ||
| // We need two layers of tryCatch here, unfortunately. The inner layer | ||
| // We need two layers of JSG_TRY here, unfortunately. The inner layer | ||
| // covers the algorithm implementation itself and is our typical error | ||
| // handling path. It ensures that if the algorithm throws an exception, | ||
| // that is properly converted in to a rejected promise that is *then* | ||
| // handled by the onFailure handler that is passed in. The outer tryCatch | ||
| // handled by the onFailure handler that is passed in. The outer JSG_TRY | ||
| // handles the rare and generally unexpected failure of the calls to | ||
| // .then() itself, which can throw JS exceptions synchronously in certain | ||
| // rare cases. For those we return a rejected promise but do not call the | ||
| // onFailure case since such errors are generally indicative of a fatal | ||
| // condition in the isolate (e.g. out of memory, other fatal exception, etc). | ||
| return js.tryCatch([&] { | ||
| JSG_TRY { | ||
| KJ_IF_SOME(ioContext, IoContext::tryCurrent()) { | ||
| return js | ||
| .tryCatch([&] { return algorithm(js, kj::fwd<decltype(args)>(args)...); }, | ||
| [&](jsg::Value&& exception) { return js.rejectedPromise<void>(kj::mv(exception)); }) | ||
| .then(js, ioContext.addFunctor(kj::mv(onSuccess)), | ||
| ioContext.addFunctor(kj::mv(onFailure))); | ||
| auto getInnerPromise = [&]() -> jsg::Promise<void> { | ||
| JSG_TRY { | ||
| return algorithm(js, kj::fwd<decltype(args)>(args)...); | ||
| } | ||
| catch (...) { | ||
| jsg::Value exception = getCaughtExceptionAsJsg(); | ||
| return js.rejectedPromise<void>(kj::mv(exception)); | ||
| } | ||
| }; | ||
| return getInnerPromise().then( | ||
| js, ioContext.addFunctor(kj::mv(onSuccess)), ioContext.addFunctor(kj::mv(onFailure))); | ||
| } else { | ||
| return js | ||
| .tryCatch([&] { return algorithm(js, kj::fwd<decltype(args)>(args)...); }, | ||
| [&](jsg::Value&& exception) { | ||
| return js.rejectedPromise<void>(kj::mv(exception)); | ||
| }).then(js, kj::mv(onSuccess), kj::mv(onFailure)); | ||
| auto getInnerPromise = [&]() -> jsg::Promise<void> { | ||
| JSG_TRY { | ||
| return algorithm(js, kj::fwd<decltype(args)>(args)...); | ||
| } | ||
| catch (...) { | ||
| jsg::Value exception = getCaughtExceptionAsJsg(); | ||
| return js.rejectedPromise<void>(kj::mv(exception)); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there are going to be some common patterns like:
Maybe these can be boilerplated with their own macros? But also just a nit.. feel free to ignore. |
||
| } | ||
| }; | ||
| return getInnerPromise().then(js, kj::mv(onSuccess), kj::mv(onFailure)); | ||
| } | ||
| }, [&](jsg::Value&& exception) { return js.rejectedPromise<void>(kj::mv(exception)); }); | ||
| } | ||
| catch (...) { | ||
| jsg::Value exception = getCaughtExceptionAsJsg(); | ||
| return js.rejectedPromise<void>(kj::mv(exception)); | ||
| } | ||
| } | ||
|
|
||
| // If the algorithm does not exist, we just handle it as a success and move on. | ||
|
|
@@ -1529,10 +1544,11 @@ jsg::Promise<void> WritableImpl<Self>::write( | |
| size_t size = 1; | ||
| KJ_IF_SOME(sizeFunc, algorithms.size) { | ||
| kj::Maybe<jsg::Value> failure; | ||
| js.tryCatch([&] { size = sizeFunc(js, value); }, [&](jsg::Value exception) { | ||
| JSG_TRY_CATCH(getSizeFuncException) try { size = sizeFunc(js, value); } catch (...) { | ||
| auto exception = getSizeFuncException(); | ||
| startErroring(js, self.addRef(), exception.getHandle(js)); | ||
| failure = kj::mv(exception); | ||
| }); | ||
| } | ||
| KJ_IF_SOME(exception, failure) { | ||
| return js.rejectedPromise<void>(kj::mv(exception)); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ | |
|
|
||
| #include "simdutf.h" | ||
|
|
||
| #include <workerd/util/exception.h> | ||
| #include <workerd/util/mimetype.h> | ||
| #include <workerd/util/strings.h> | ||
|
|
||
|
|
@@ -69,10 +70,10 @@ namespace { | |
|
|
||
| template <typename Func> | ||
| auto translateTeeErrors(Func&& f) -> decltype(kj::fwd<Func>(f)()) { | ||
| try { | ||
| KJ_TRY { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An example of Ultimately, I think I prefer the raw syntax, myself.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. agreed on prefering the raw syntax. |
||
| co_return co_await f(); | ||
| } catch (...) { | ||
| auto exception = kj::getCaughtExceptionAsKj(); | ||
| } | ||
| KJ_CATCH(kj::Exception & exception) { | ||
| KJ_IF_SOME(e, | ||
| translateKjException(exception, | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,10 +191,27 @@ struct FunctionContext: public ContextGlobalObject { | |
| }); | ||
| } | ||
|
|
||
| kj::String testTryCatch2(Lock& js, jsg::Function<int()> thrower) { | ||
| // Here we prove that the macro is if-else friendly. | ||
| // Note that clang-format doesn't recognize it as a `try`, so we get wonky formatting. Oh well. | ||
| if (true) JSG_TRY { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An example of how
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm.. can clang-format be extended/updated to account for this I wonder? |
||
| return kj::str(thrower(js)); | ||
| } | ||
| catch (...) { | ||
| Value exception = getCaughtExceptionAsJsg(); | ||
| auto handle = exception.getHandle(js); | ||
| return kj::str("caught: ", handle); | ||
| } | ||
| else { | ||
| KJ_UNREACHABLE; | ||
| } | ||
| } | ||
|
|
||
| JSG_RESOURCE_TYPE(FunctionContext) { | ||
| JSG_METHOD(test); | ||
| JSG_METHOD(test2); | ||
| JSG_METHOD(testTryCatch); | ||
| JSG_METHOD(testTryCatch2); | ||
|
|
||
| JSG_READONLY_PROTOTYPE_PROPERTY(square, getSquare); | ||
| JSG_READONLY_PROTOTYPE_PROPERTY(gcLambda, getGcLambda); | ||
|
|
@@ -220,6 +237,9 @@ KJ_TEST("jsg::Function<T>") { | |
|
|
||
| e.expectEval("testTryCatch(() => { return 123; })", "string", "123"); | ||
| e.expectEval("testTryCatch(() => { throw new Error('foo'); })", "string", "caught: Error: foo"); | ||
|
|
||
| e.expectEval("testTryCatch2(() => { return 123; })", "string", "123"); | ||
| e.expectEval("testTryCatch2(() => { throw new Error('foo'); })", "string", "caught: Error: foo"); | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2995,6 +2995,123 @@ inline Value SelfRef::asValue(Lock& js) const { | |
| return Value(js.v8Isolate, getHandle(js).As<v8::Value>()); | ||
| } | ||
|
|
||
| namespace _ { | ||
|
|
||
| // Helper class for converting caught exceptions to JSG Values in catch blocks. | ||
| // This is used internally by the JSG_TRY macro to provide clean exception handling | ||
| // that works with both JavaScript exceptions and KJ exceptions. | ||
| // | ||
| // The class automatically sets up a v8::TryCatch when constructed and can convert | ||
| // any caught exception to a jsg::Value when called. It handles: | ||
| // - JsExceptionThrown: Returns the V8 exception value directly | ||
| // - kj::Exception: Converts to JS using Lock::exceptionToJs() | ||
| // | ||
| // Usage is typically through JSG_TRY macro: | ||
| // JSG_TRY { | ||
| // someCodeThatMightThrow(); | ||
| // } | ||
| // catch (...) { | ||
| // auto jsException = getCaughtExceptionAsJsg(); | ||
| // // Handle the JS-converted exception | ||
| // } | ||
| // | ||
| // IMPORTANT: The operator() can only be called once per instance, as it consumes | ||
| // the internal TryCatch holder. Subsequent calls will throw. | ||
| // | ||
| // WARNING: Never call the exception converter function in the try block! | ||
| // It will fail with a raw `throw` because there's no active exception to re-throw. | ||
| // Only call it from within catch blocks where an exception has actually been caught. | ||
| class GetCaughtExceptionAsJsg { | ||
| public: | ||
| GetCaughtExceptionAsJsg() { | ||
| maybeHolder.emplace(Lock::current().v8Isolate); | ||
| } | ||
|
|
||
| // Handle any in-flight JsExceptionThrown or kj::Exception. If a JsExceptionThrown is found, our | ||
| // v8::TryCatch's currently held exception value is returned. If a kj::Exception is found, it is | ||
| // converted to a JS value by passing it and `options` to `Lock::exceptionToJs()`. | ||
| Value operator()(ExceptionToJsOptions options = {}) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a fairly non-trivial function in a non-templated class. Can it perhaps be moved into jsg.c++? |
||
| // This function always consumes the holder. | ||
| KJ_DEFER(maybeHolder = kj::none); | ||
| auto& tryCatch = | ||
| KJ_REQUIRE_NONNULL(maybeHolder, "getCaughtExceptionAsJsg() can only be called once") | ||
| .tryCatch; | ||
|
|
||
| // Same logic as that found in `jsg::Lock::tryCatch()`. | ||
| try { | ||
| throw; | ||
| } catch (JsExceptionThrown&) { | ||
| if (!tryCatch.CanContinue() || !tryCatch.HasCaught() || tryCatch.Exception().IsEmpty()) { | ||
| tryCatch.ReThrow(); | ||
| throw; | ||
| } | ||
| return Value(Lock::current().v8Isolate, tryCatch.Exception()); | ||
| } catch (kj::Exception& e) { | ||
| return Lock::current().exceptionToJs(kj::mv(e), options); | ||
| } | ||
| } | ||
|
|
||
| private: | ||
| // Simple wrapper to work around v8::TryCatch's deleted operator new. | ||
| struct Holder { | ||
| v8::TryCatch tryCatch; | ||
| explicit Holder(v8::Isolate* isolate): tryCatch(isolate) {} | ||
| }; | ||
|
|
||
| kj::Maybe<Holder> maybeHolder; | ||
| }; | ||
|
|
||
| } // namespace _ | ||
|
|
||
| // General-purpose macro for introducing scoped variables into a statement. Uses the | ||
| // KJ_IF_SOME-style scoping trick where the variable is declared in an if condition that's always | ||
| // false, followed by an else that executes the actual statement. This allows the variable to be in | ||
| // scope for the entire statement, and more importantly to go out of scope at the end of the | ||
| // statement without needing braces. The scope is tied to the statement itself, not to a block. | ||
| // | ||
| // It is valid to use multiple KJ_STMT_SCOPED macros for a single statement. | ||
| // | ||
| // TODO(now): Move to libkj. | ||
| #define KJ_STMT_SCOPED(decl) \ | ||
| if (decl; false) { \ | ||
| } else | ||
|
|
||
| // JSG_TRY_CATCH macro for exception handling with a custom-named exception converter. | ||
| // Unlike JSG_TRY, this doesn't include the `try` keyword, giving more explicit control. | ||
| // More transparent to colleagues and clang-format since it shows raw try-catch syntax. | ||
| // | ||
| // Usage: | ||
| // JSG_TRY_CATCH(getCaughtExceptionAsJsg) try { | ||
| // someCodeThatMightThrow(); | ||
| // } catch (...) { | ||
| // jsg::Value exception = getCaughtExceptionAsJsg(); | ||
| // // Handle the JS-converted exception | ||
| // } | ||
| // | ||
| // WARNING: Never call the exception converter function in the try block! | ||
| // It will fail with a raw `throw` because there's no active exception to re-throw. | ||
| // Only call it from within catch blocks where an exception has actually been caught. | ||
| #define JSG_TRY_CATCH(name) KJ_STMT_SCOPED(::workerd::jsg::_::GetCaughtExceptionAsJsg name) | ||
|
|
||
| // JSG_TRY is a convenience macro implemented in terms of JSG_TRY_CATCH. | ||
| // It automatically names the exception converter `getCaughtExceptionAsJsg` and includes the `try` | ||
| // keyword. | ||
| // | ||
| // Note: This macro doesn't play well with clang-format due to the embedded `try` keyword. | ||
| // | ||
| // Usage: | ||
| // JSG_TRY { | ||
| // someCodeThatMightThrow(); | ||
| // } catch (...) { | ||
| // jsg::Value exception = getCaughtExceptionAsJsg(); | ||
| // // Handle the JS-converted exception | ||
| // } | ||
| // | ||
| // WARNING: Never call getCaughtExceptionAsJsg() in the try block! | ||
| // It will fail with a raw `throw` because there's no active exception to re-throw. | ||
| // Only call it from within catch blocks where an exception has actually been caught. | ||
| #define JSG_TRY JSG_TRY_CATCH(getCaughtExceptionAsJsg) try | ||
|
|
||
| } // namespace workerd::jsg | ||
|
|
||
| // clang-format off | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: I appreciate that the structure is similar to try/catch syntax but the
catch(...)there just feels.. odd.. for some reason. Feel free to ignore but given that the macro is a bit special I'd almost prefer something like...But even that feels bleh, lol. So feel free to ignore this 😆