Skip to content
Merged
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
7 changes: 5 additions & 2 deletions src/workerd/api/crypto/crypto.c++
Original file line number Diff line number Diff line change
Expand Up @@ -806,13 +806,16 @@ DigestStream::DigestStream(kj::Own<WritableStreamController> controller,
state(Ready(kj::mv(algorithm), kj::mv(resolver))) {}

void DigestStream::dispose(jsg::Lock& js) {
js.tryCatch([&] {
JSG_TRY(js) {
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)); });
}
JSG_CATCH(exception) {
js.throwException(kj::mv(exception));
}
}

void DigestStream::visitForMemoryInfo(jsg::MemoryTracker& tracker) const {
Expand Down
7 changes: 4 additions & 3 deletions src/workerd/api/memory-cache.c++
Original file line number Diff line number Diff line change
Expand Up @@ -430,11 +430,12 @@ 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(js) {
jsg::Serializer serializer(js);
serializer.write(js, value.getHandle(js));
return kj::atomicRefcounted<CacheValue>(serializer.release().data);
}, [&](jsg::Value&& exception) -> kj::Own<CacheValue> {
}
JSG_CATCH(exception) {
// 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 +451,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(js) {
auto message = js.alloc<MessageEvent>(js, kj::str("message"), value, kj::String(), JSG_THIS);
dispatchEventImpl(js, kj::mv(message));
}, [&](jsg::Value exception) {
}
JSG_CATCH(exception) {
// There was an error dispatching the message event.
// We will dispatch a messageerror event instead.
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
47 changes: 31 additions & 16 deletions src/workerd/api/streams/standard.c++
Original file line number Diff line number Diff line change
Expand Up @@ -505,31 +505,43 @@ 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(js) {
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(js) {
return algorithm(js, kj::fwd<decltype(args)>(args)...);
}
JSG_CATCH(exception) {
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(js) {
return algorithm(js, kj::fwd<decltype(args)>(args)...);
}
JSG_CATCH(exception) {
return js.rejectedPromise<void>(kj::mv(exception));
}
};
return getInnerPromise().then(js, kj::mv(onSuccess), kj::mv(onFailure));
}
}, [&](jsg::Value&& exception) { return js.rejectedPromise<void>(kj::mv(exception)); });
}
JSG_CATCH(exception) {
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 @@ -1628,10 +1640,13 @@ 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(js) {
size = sizeFunc(js, value);
}
JSG_CATCH(exception) {
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 @@ -19,9 +19,12 @@ std::optional<URLPattern::URLPatternRegexEngine::regex_type> URLPattern::URLPatt
// std::string_view is not guaranteed to be null-terminated, but kj::StringPtr requires it.
// We need to create a null-terminated copy.
auto str = kj::str(kj::arrayPtr(pattern.data(), pattern.size()));
return js.tryCatch([&]() -> std::optional<regex_type> {
JSG_TRY(js) {
return jsg::JsRef(js, js.regexp(str, flags));
}, [&](auto reason) -> std::optional<regex_type> { return std::nullopt; });
}
JSG_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(js) {
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> {
}
JSG_CATCH(_) {
JSG_FAIL_REQUIRE(TypeError, "Invalid regular expression syntax.");
});
}
}

jsg::Ref<URLPattern> create(jsg::Lock& js, jsg::UrlPattern pattern) {
Expand Down
44 changes: 36 additions & 8 deletions src/workerd/jsg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ built around them.

In order to execute JavaScript on the current thread, a lock must be acquired on the `v8::Isolate`.
The `jsg::Lock&` represents the current lock. It is passed as an argument to many methods that
require access to the JavaScript isolate and context.
require access to the JavaScript isolate and context. By convention, this argument is always named
`js`.

The `jsg::Lock` interface itself provides access to basic JavaScript functionality, such as the
ability to construct basic JavaScript values and call JavaScript functions.
Expand Down Expand Up @@ -2534,14 +2535,11 @@ The `jsErrorType` parameter can be one of:
Unlike `KJ_REQUIRE`, `JSG_REQUIRE` passes all message arguments through `kj::str()`, so you are
responsible for formatting the entire message string.

#### `JsExceptionThrown`
#### `js.error()`, `js.throwException()`, and `JsExceptionThrown`

When C++ code needs to throw a JavaScript exception, it should:
1. Call `isolate->ThrowException()` to set the JavaScript error value
2. Throw `JsExceptionThrown()` as a C++ exception

This C++ exception is caught by JSG's callback glue before returning to V8. This approach is
more ergonomic than V8's convention of returning `v8::Maybe` values.
When C++ code needs to throw a JavaScript exception:
1. Create the error object with `js.error("Error reason")`
2. Throw using `js.throwException()`

```cpp
void someMethod(jsg::Lock& js) {
Expand All @@ -2554,6 +2552,36 @@ void someMethod(jsg::Lock& js) {
}
```

Under the hood, `js.throwException()` uses V8's lower level API, `isolate->ThrowException()`, to
throw the exception in the V8 engine. It then throws a special C++ object of type
`JsExceptionThrown`, whose purpose is to unwind the C++ stack back to the point where JavaScript
called into C++. This C++ exception is caught by JSG's callback glue before returning to V8. This
approach is more ergonomic than V8's convention of returning `v8::Maybe` values.

#### `JSG_TRY` and `JSG_CATCH`

JSG provides `JSG_TRY` and `JSG_CATCH` macros which replace the normal `try` and `catch` keywords
when you need to catch exceptions as JavaScript exceptions. Each take one argument: `JSG_TRY` takes
the `jsg::Lock&` reference, and `JSG_CATCH` takes your desired variable name for the caught
exception.

```cpp
void someMethod(jsg::Lock& js) {
JSG_TRY(js) {
someThrowyCode();
}
JSG_CATCH(e) {
// Just rethrow.
js.throwException(kj::mv(e));
}
}
```

The example above actually illustrates a common, useful scenario of coercing any exception thrown
into a JavaScript Error object. That is, if `someThrowyCode()` in the example above throws a KJ C++
exception, `JSG_CATCH(e)` will catch it and convert it to a JavaScript error by calling
`js.exceptionToJs()`.

#### `makeInternalError()` and `throwInternalError()`

These functions create JavaScript errors from internal C++ exceptions while obfuscating
Expand Down
117 changes: 117 additions & 0 deletions src/workerd/jsg/function-test.c++
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,76 @@ struct FunctionContext: public ContextGlobalObject {
});
}

kj::String testTryCatch2(Lock& js, jsg::Function<int()> thrower) {
// Here we prove that the macro is if-else friendly.
if (true) JSG_TRY(js) {
return kj::str(thrower(js));
}
JSG_CATCH(exception) {
auto handle = exception.getHandle(js);
return kj::str("caught: ", handle);
}
else {
KJ_UNREACHABLE;
}
}

kj::String testTryCatchWithOptions(Lock& js, jsg::Function<void()> thrower) {
// Test that JSG_CATCH can accept ExceptionToJsOptions.
JSG_TRY(js) {
thrower(js);
return kj::str("no exception");
}
JSG_CATCH(exception, {.ignoreDetail = true}) {
auto handle = exception.getHandle(js);
return kj::str("caught with options: ", handle);
}
}

kj::String testNestedTryCatchInnerCatches(Lock& js, jsg::Function<void()> thrower) {
// Test nested JSG_TRY/JSG_CATCH where inner catches, outer doesn't see exception.
JSG_TRY(js) {
kj::String innerResult;
JSG_TRY(js) {
thrower(js);
innerResult = kj::str("inner: no exception");
}
JSG_CATCH(innerException) {
innerResult = kj::str("inner caught: ", innerException.getHandle(js));
}
return kj::str("outer: no exception, ", innerResult);
}
JSG_CATCH(outerException) {
return kj::str("outer caught: ", outerException.getHandle(js));
}
}

kj::String testNestedTryCatchOuterCatches(Lock& js, jsg::Function<void()> thrower) {
// Test nested JSG_TRY/JSG_CATCH where inner rethrows, outer catches.
JSG_TRY(js) {
JSG_TRY(js) {
thrower(js);
return kj::str("inner: no exception");
}
JSG_CATCH(innerException) {
// Rethrow so outer can catch
js.throwException(kj::mv(innerException));
}
return kj::str("outer: no exception");
}
JSG_CATCH(outerException) {
return kj::str("outer caught: ", outerException.getHandle(js));
}
}

JSG_RESOURCE_TYPE(FunctionContext) {
JSG_METHOD(test);
JSG_METHOD(test2);
JSG_METHOD(testTryCatch);
JSG_METHOD(testTryCatch2);
JSG_METHOD(testTryCatchWithOptions);
JSG_METHOD(testNestedTryCatchInnerCatches);
JSG_METHOD(testNestedTryCatchOuterCatches);

JSG_READONLY_PROTOTYPE_PROPERTY(square, getSquare);
JSG_READONLY_PROTOTYPE_PROPERTY(gcLambda, getGcLambda);
Expand All @@ -220,6 +286,57 @@ 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");

e.expectEval("testTryCatchWithOptions(() => {})", "string", "no exception");
e.expectEval("testTryCatchWithOptions(() => { throw new Error('bar'); })", "string",
"caught with options: Error: bar");

// Nested JSG_TRY/JSG_CATCH tests
e.expectEval("testNestedTryCatchInnerCatches(() => {})", "string",
"outer: no exception, inner: no exception");
e.expectEval("testNestedTryCatchInnerCatches(() => { throw new Error('inner'); })", "string",
"outer: no exception, inner caught: Error: inner");

e.expectEval("testNestedTryCatchOuterCatches(() => {})", "string", "inner: no exception");
e.expectEval("testNestedTryCatchOuterCatches(() => { throw new Error('rethrown'); })", "string",
"outer caught: Error: rethrown");
}

KJ_TEST("JSG_TRY/JSG_CATCH with TerminateExecution") {
Evaluator<FunctionContext, FunctionIsolate> e(v8System);

// TerminateExecution should propagate through JSG_CATCH without being caught.
// The Evaluator's run() method will detect the termination and throw.
KJ_EXPECT_THROW_MESSAGE("TerminateExecution() was called", e.run([](auto& js) {
// Test single-level JSG_TRY/JSG_CATCH with TerminateExecution
JSG_TRY(js) {
js.terminateExecutionNow();
}
JSG_CATCH(exception) {
(void)exception;
KJ_FAIL_ASSERT("TerminateExecution was caught by JSG_CATCH");
}
}));

KJ_EXPECT_THROW_MESSAGE("TerminateExecution() was called", e.run([](auto& js) {
// Test nested JSG_TRY/JSG_CATCH with TerminateExecution - should propagate through both
JSG_TRY(js) {
JSG_TRY(js) {
js.terminateExecutionNow();
}
JSG_CATCH(innerException) {
(void)innerException;
KJ_FAIL_ASSERT("TerminateExecution was caught by inner JSG_CATCH");
}
}
JSG_CATCH(outerException) {
(void)outerException;
KJ_FAIL_ASSERT("TerminateExecution was caught by outer JSG_CATCH");
}
}));
}

} // namespace
Expand Down
Loading
Loading