Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/workerd/api/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,7 @@ wd_cc_library(
srcs = ["util.c++"],
hdrs = ["util.h"],
implementation_deps = [
"//src/workerd/util:exception",
"//src/workerd/util:mimetype",
"//src/workerd/util:strings",
"@simdutf",
Expand Down
8 changes: 6 additions & 2 deletions src/workerd/api/crypto/crypto.c++
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Collaborator

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...

JSG_TRY(
  {
    // Block to try
  },
  {
    // Catch block
  }
)

But even that feels bleh, lol. So feel free to ignore this 😆

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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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...

JSG_CATCH(exception) {
  js.throwException(kj::mv(exception));
}

To boilerplate this particular piece might be handy?

js.throwException(kj::mv(exception));
}
}

void DigestStream::visitForMemoryInfo(jsg::MemoryTracker& tracker) const {
Expand Down
8 changes: 5 additions & 3 deletions src/workerd/api/memory-cache.c++
Original file line number Diff line number Diff line change
Expand Up @@ -430,11 +430,13 @@ void SharedMemoryCache::Use::delete_(const kj::String& key) const {
// Attempts to serialize a JavaScript value. If that fails, this function throws
// a tunneled exception, see jsg::createTunneledException().
static kj::Own<CacheValue> hackySerialize(jsg::Lock& js, jsg::JsRef<jsg::JsValue>& value) {
return js.tryCatch([&]() -> kj::Own<CacheValue> {
JSG_TRY {
jsg::Serializer serializer(js);
serializer.write(js, value.getHandle(js));
return kj::atomicRefcounted<CacheValue>(serializer.release().data);
}, [&](jsg::Value&& exception) -> kj::Own<CacheValue> {
}
catch (...) {
jsg::Value exception = getCaughtExceptionAsJsg();
// We run into big problems with tunneled exceptions here. When
// the toString() function of the JavaScript error is not marked
// as side effect free, tunneling the exception fails entirely
Expand All @@ -450,7 +452,7 @@ static kj::Own<CacheValue> hackySerialize(jsg::Lock& js, jsg::JsRef<jsg::JsValue
// This is still pretty bad. We lose the original error stack.
// TODO(later): remove string-based error tunneling
throw js.exceptionToKj(kj::mv(exception));
});
}
}

jsg::Promise<jsg::JsRef<jsg::JsValue>> MemoryCache::read(jsg::Lock& js,
Expand Down
7 changes: 4 additions & 3 deletions src/workerd/api/messagechannel.c++
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,18 @@ MessagePort::MessagePort()
}

void MessagePort::dispatchMessage(jsg::Lock& js, const jsg::JsValue& value) {
js.tryCatch([&] {
JSG_TRY_CATCH(getMessageException) try {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example of how JSG_TRY_CATCH looks in practice. I like this better than JSG_TRY, because clang-format can see the full try-catch statement.

A version of JSG_TRY which includes the default name (getCaughtExceptionAsJsg) but doesn't include the try keyword could be a third option:

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
Expand Down
48 changes: 32 additions & 16 deletions src/workerd/api/streams/standard.c++
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are going to be some common patterns like:

  1. Convert the error and throw it
  2. Convert the error and return a rejected promise

Maybe these can be boilerplated with their own macros?

JSG_CATCH_THROW()  // short cut for rethrowing
JSG_CATCH_RETURN_REJECTED()  // short cut for returning rejected

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.
Expand Down Expand Up @@ -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));
}
Expand Down
7 changes: 5 additions & 2 deletions src/workerd/api/urlpattern-standard.c++
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@ std::optional<URLPattern::URLPatternRegexEngine::regex_type> URLPattern::URLPatt
flags | static_cast<int>(jsg::Lock::RegExpFlags::kIGNORE_CASE));
}

return js.tryCatch([&]() -> std::optional<regex_type> {
JSG_TRY {
return jsg::JsRef(js, js.regexp(kj::StringPtr(pattern.data(), pattern.size()), flags));
}, [&](auto reason) -> std::optional<regex_type> { return std::nullopt; });
}
catch (...) {
return std::nullopt;
}
}

bool URLPattern::URLPatternRegexEngine::regex_match(
Expand Down
7 changes: 4 additions & 3 deletions src/workerd/api/urlpattern.c++
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ namespace workerd::api {
namespace {
jsg::JsRef<jsg::JsRegExp> compileRegex(
jsg::Lock& js, const jsg::UrlPattern::Component& component, bool ignoreCase) {
return js.tryCatch([&] {
JSG_TRY {
jsg::Lock::RegExpFlags flags = jsg::Lock::RegExpFlags::kUNICODE;
if (ignoreCase) {
flags = static_cast<jsg::Lock::RegExpFlags>(
flags | static_cast<int>(jsg::Lock::RegExpFlags::kIGNORE_CASE));
}
return jsg::JsRef<jsg::JsRegExp>(js, js.regexp(component.getRegex(), flags));
}, [&](auto reason) -> jsg::JsRef<jsg::JsRegExp> {
}
catch (...) {
JSG_FAIL_REQUIRE(TypeError, "Invalid regular expression syntax.");
});
}
}

jsg::Ref<URLPattern> create(jsg::Lock& js, jsg::UrlPattern pattern) {
Expand Down
7 changes: 4 additions & 3 deletions src/workerd/api/util.c++
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include "simdutf.h"

#include <workerd/util/exception.h>
#include <workerd/util/mimetype.h>
#include <workerd/util/strings.h>

Expand Down Expand Up @@ -69,10 +70,10 @@ namespace {

template <typename Func>
auto translateTeeErrors(Func&& f) -> decltype(kj::fwd<Func>(f)()) {
try {
KJ_TRY {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example of KJ_TRY and KJ_CATCH at work.

Ultimately, I think I prefer the raw syntax, myself.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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,
{
Expand Down
20 changes: 20 additions & 0 deletions src/workerd/jsg/function-test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An example of how JSG_TRY looks like in practice. Note that clang-format doesn't recognize JSG_TRY as a try keyword, so the formatting is messed up -- the catch is always formatted as if starting a new statement.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
Expand All @@ -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
Expand Down
117 changes: 117 additions & 0 deletions src/workerd/jsg/jsg.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Expand Down
Loading
Loading