Skip to content

feat: fix qjs bugs , use JSException to distinguish error between js and cpp#61

Merged
Seele-Official merged 10 commits intoclice-io:mainfrom
Seele-Official:qjs-fix
Mar 18, 2026
Merged

feat: fix qjs bugs , use JSException to distinguish error between js and cpp#61
Seele-Official merged 10 commits intoclice-io:mainfrom
Seele-Official:qjs-fix

Conversation

@Seele-Official
Copy link
Copy Markdown
Collaborator

@Seele-Official Seele-Official commented Mar 18, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Consistent unwrapping of optional values to avoid unexpected "nothing" short-circuits.
  • Improvements

    • Enhanced error and exception reporting with richer metadata and clearer messages.
    • Adjusted logging format for API call names for improved readability.
  • Documentation

    • Public API reference removed and replaced with a placeholder pending rewrite.
  • Tests

    • Added and updated unit tests to cover enhanced error/json behaviors and error formatting.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 18, 2026

📝 Walkthrough

Walkthrough

Adds new QuickJS error types (TypeException, JSException, Error) and switches many throw/rejection paths to use JSException::dump(ctx); simplifies JS rejection formatting; removes large TypeScript API declarations in doc/api.md; small logging, optional-property, import, and test adjustments.

Changes

Cohort / File(s) Summary
QJS error surface
src/catter/core/qjs.h
Introduces public TypeException, JSException, and Error; extends Exception with new constructors/formatting; replaces TypeError uses and updates many throw sites to JSException::dump(ctx).
JS binding & rejection formatting
src/catter/core/js.cc, tests/unit/catter/core/js.cc
Removes several inline helper formatters, inlines per-argument rejection trace assembly, wraps rejection formatting in try/catch, and updates tests to use std::string::contains.
Public API docs removal
doc/api.md
Replaces extensive TypeScript API/type declarations and examples with a TODO placeholder, removing documented public API surface from the file.
TypeScript import change
api/src/util/cmd.ts
Changes import style to a namespace import from ../option/index.js (previously a named import from ../index.js).
Small internal tweaks
src/catter/core/capi/type.h, src/catter/core/apitool.h
Removes an extra is_nothing() check for optional property reading; adjusts log format strings to wrap API names in backticks.
Tests and minor refactor
tests/unit/catter/core/qjs.cc, tests/unit/common/opt/clang.cc
Adds a new qjs test covering error metadata and JSON helpers; reformats a clang option test into a single TEST_CASE with inner scopes; adds a small include.

Sequence Diagram(s)

sequenceDiagram
    participant Caller as Client (C++)
    participant Func as qjs::Function
    participant QJS as QuickJS Runtime
    participant Err as JS Error/Object
    participant JSEx as catter::qjs::JSException
    participant Logger as Logger

    Caller->>Func: call(...)
    Func->>QJS: execute JS code
    QJS-->>Err: throws / rejects (Error value)
    QJS->>JSEx: JSException::dump(ctx) (build JSException/Error)
    JSEx-->>Func: propagate exception
    Func-->>Caller: throw JSException
    Caller->>Logger: log error (formatted with backticks)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • star-hengxing
  • 16bit-ykiko

Poem

🐰 I nibble at stacks with curious cheer,
New Errors and JSExceptions appear.
I stitch traces, hop through tests so bright,
Backticks in logs, and JSON set right. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.49% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title references fixing qjs bugs and using JSException to distinguish errors, which aligns with the main changes in src/catter/core/qjs.h and related files, but lacks clarity due to formatting issues and grammatical inconsistencies.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/catter/core/js.cc (1)

106-125: ⚠️ Potential issue | 🟠 Major

Preserve non-Error promise rejection reasons.

Both callbacks force every rejection argument through arg.as<qjs::Error>(). If the rejection reason is a string, number, null, or a plain object, that conversion throws inside the callback and the wrapper ends up reporting its own failure instead of the original rejection.

Suggested direction
+        auto append_rejection = [&](const qjs::Value& arg) {
+            if(auto error = arg.to<qjs::Error>(); error.has_value()) {
+                error_strace += error->format() + "\n";
+                return;
+            }
+            if(auto text = arg.to<std::string>(); text.has_value()) {
+                error_strace += *text + "\n";
+                return;
+            }
+            error_strace += "<non-Error rejection>\n";
+        };
+
         auto reject = CallBack::from(js_ctx, [&](qjs::Parameters args) {
             state = Rejected;
             for(auto& arg: args) {
-                error_strace += arg.as<qjs::Error>().format() + "\n";
+                append_rejection(arg);
             }
         });
@@
         auto catch_fn = CallBack::from(js_ctx, [&](qjs::Parameters args) {
             state = Rejected;
             try {
                 for(auto& arg: args) {
-                    error_strace += arg.as<qjs::Error>().format() + "\n";
+                    append_rejection(arg);
                 }
             } catch(const std::exception& e) {
                 error_strace += std::format("Exception: {}\n", e.what());
             }
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/catter/core/js.cc` around lines 106 - 125, The reject and catch_fn
callbacks currently call arg.as<qjs::Error>() unconditionally which throws for
non-Error rejection reasons; change both lambdas (the CallBack::from handlers
named reject and catch_fn) to detect/handle non-Error values instead of forcing
as<qjs::Error>() — e.g. check if arg is an Error (or use a safe conversion API)
and if so append its formatted stack, otherwise convert the value to a stable
string (JSON.stringify or the JS ToString representation) and append that to
error_strace; ensure you don't let the conversion throw so the original
rejection reason is preserved.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@doc/api.md`:
- Line 1: doc/api.md currently contains a placeholder "TODO" which removes the
published API contract; replace it with the actual API documentation for the
public error surface and any changed exports so plugin authors have an upgrade
contract (restore the previous API content or regenerate the API doc to include
current exported functions/classes/errors and their signatures, and add a short
"Breaking changes / migration" section or link to the changelog/migration
guide). Ensure the file documents all public exports and error types referenced
by the codebase so consumers can rely on it.

In `@src/catter/core/capi/type.h`:
- Around line 129-133: The current logic around reading optional fields lets a
Value::Null slip through to from_property_value and throw; in the function using
object.get_optional_property(std::string(property_name)) (the optional_value
local), update the guard to return std::nullopt not only when
optional_value.has_value() is false but also when the contained Value is null
(e.g., optional_value->is_null() or equivalent), so that fields like { parent:
null } correctly map to std::nullopt before calling
from_property_value<value_type>(*optional_value).

In `@src/catter/core/qjs.h`:
- Around line 470-472: JSException::dump currently assumes the pending exception
is an Error; change it to first fetch the exception with JS_GetException(ctx)
and check JS_IsError(ctx, ex); if it's an Error keep the existing Error(ctx, ex)
path, otherwise convert the thrown value to a string (e.g. via
JS_ToString/JS_ToCString or JSON stringify) and construct the diagnostic from
that string (or wrap it into a new Error object) so non-Error throws like throw
"boom" or throw 1 are handled; make sure to manage JS values/cstrings lifetime
with JS_DupValue/JS_FreeValue/JS_FreeCString as appropriate.

In `@tests/unit/catter/core/qjs.cc`:
- Line 1: Remove the unused include by deleting the line containing "#include
<print>" from the file; this header is not referenced elsewhere in this test
(qjs.cc) so simply remove that include to keep includes clean.

---

Outside diff comments:
In `@src/catter/core/js.cc`:
- Around line 106-125: The reject and catch_fn callbacks currently call
arg.as<qjs::Error>() unconditionally which throws for non-Error rejection
reasons; change both lambdas (the CallBack::from handlers named reject and
catch_fn) to detect/handle non-Error values instead of forcing as<qjs::Error>()
— e.g. check if arg is an Error (or use a safe conversion API) and if so append
its formatted stack, otherwise convert the value to a stable string
(JSON.stringify or the JS ToString representation) and append that to
error_strace; ensure you don't let the conversion throw so the original
rejection reason is preserved.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 73adaeb2-b490-4ffb-a2f5-cc1a681ac04f

📥 Commits

Reviewing files that changed from the base of the PR and between da1ceca and 4790b19.

📒 Files selected for processing (8)
  • api/src/util/cmd.ts
  • doc/api.md
  • src/catter/core/apitool.h
  • src/catter/core/capi/type.h
  • src/catter/core/js.cc
  • src/catter/core/qjs.h
  • tests/unit/catter/core/js.cc
  • tests/unit/catter/core/qjs.cc

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/catter/core/js.cc`:
- Around line 106-111: The reject callback created via CallBack::from currently
calls arg.as<qjs::Error>() for each argument which can throw when the rejection
value is not an Error; wrap the per-argument conversion in a try-catch
(mirroring catch_fn) so you first attempt to convert to qjs::Error and on
exception fallback to using arg.to_string() (or a safe string conversion) before
appending to error_strace, keeping the state = Rejected behavior intact and
ensuring non-Error rejection values do not crash the code.

In `@tests/unit/catter/core/qjs.cc`:
- Line 508: The test calls a non-existent Error::format(); update the assertion
to use the actual method Error::to_string() instead — replace the use of
error.format() with error.to_string() in the EXPECT_TRUE check (the test around
EXPECT_TRUE(error.format().contains("Stack Trace:"))), ensuring the string
containment assertion still checks the returned std::string from
Error::to_string().

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad592061-687b-4fe2-9246-a9975cdcf710

📥 Commits

Reviewing files that changed from the base of the PR and between ad836d7 and 9d80813.

📒 Files selected for processing (3)
  • src/catter/core/js.cc
  • src/catter/core/qjs.h
  • tests/unit/catter/core/qjs.cc

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
src/catter/core/js.cc (1)

106-111: ⚠️ Potential issue | 🟠 Major

Missing try-catch in reject callback for non-Error rejection values.

The reject callback at lines 106-111 calls arg.as<qjs::Error>().format() without exception handling, while the catch_fn callback below (lines 117-126) correctly wraps the same pattern in a try-catch block.

When a promise is rejected with a non-Error value (e.g., Promise.reject("string") or Promise.reject(42)), arg.as<qjs::Error>() will throw TypeException because object_trans<Error>::as() checks JS_IsError() (see qjs.h:1249-1254). This will crash instead of capturing the rejection reason.

🐛 Proposed fix: Add try-catch like catch_fn
         auto reject = CallBack::from(js_ctx, [&](qjs::Parameters args) {
             state = Rejected;
-            for(auto& arg: args) {
-                error_strace += arg.as<qjs::Error>().format() + "\n";
-            }
+            try {
+                for(auto& arg: args) {
+                    error_strace += arg.as<qjs::Error>().format() + "\n";
+                }
+            } catch(const std::exception& e) {
+                error_strace += std::format("Exception: {}\n", e.what());
+            }
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/catter/core/js.cc` around lines 106 - 111, The reject callback created
with CallBack::from (the lambda that sets state = Rejected and appends to
error_strace by calling arg.as<qjs::Error>().format()) needs the same try-catch
protection as the existing catch_fn: wrap the arg.as<qjs::Error>().format() call
in a try-catch so non-Error rejection values (strings, numbers) don't throw
TypeException; on exception, fall back to a safe string representation (e.g.,
convert arg to string or use a generic message) and append that to error_strace,
mirroring the catch_fn error-handling pattern.
src/catter/core/qjs.h (1)

470-472: ⚠️ Potential issue | 🟠 Major

JSException::dump() assumes all JS exceptions are Error objects.

This is a known issue from a previous review. When JavaScript throws a non-Error value (e.g., throw "boom", throw 1, or throw {}), constructing Error(ctx, JS_GetException(ctx)) and calling format() will fail because Error::message(), Error::stack(), and Error::name() call .as<std::string>() on properties that may not exist or may not be strings.

🐛 Proposed fix with fallback for non-Error throws
 inline JSException JSException::dump(JSContext* ctx) {
-    return JSException(Error(ctx, JS_GetException(ctx)));
+    JSValue ex = JS_GetException(ctx);
+    if(JS_IsError(ctx, ex)) {
+        return JSException(Error(ctx, std::move(ex)));
+    }
+    // Fallback for non-Error thrown values
+    const char* str = JS_ToCString(ctx, ex);
+    std::string msg = str ? str : "<non-Error JS exception>";
+    if(str) JS_FreeCString(ctx, str);
+    JS_FreeValue(ctx, ex);
+    return JSException(Exception(std::move(msg)));
 }

Note: This requires adding a constructor JSException(Exception&&) or JSException(std::string&&) to handle the non-Error case.

🧹 Nitpick comments (2)
src/catter/core/qjs.h (2)

1309-1314: Function arity check may throw JSException unexpectedly.

Line 1312 calls obj.get_property("length").as<int64_t>() which can throw JSException if the property access fails (e.g., due to a getter that throws). This exception would be unexpected when the intent is to throw TypeException for type mismatches.

♻️ Consider using optional conversion
-        if(obj.get_property("length").as<int64_t>() != sizeof...(Args)) {
+        auto len = obj.get_property("length").to<int64_t>();
+        if(!len.has_value() || *len != sizeof...(Args)) {
             throw TypeException("Function has incorrect number of arguments");
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/catter/core/qjs.h` around lines 1309 - 1314, The arity check currently
calls obj.get_property("length").as<int64_t>() which can throw JSException; wrap
the property access and conversion in a try/catch that catches JSException (or
use a safe/optional retrieval API if available) and map any failure to the
intended TypeException so callers always get TypeException for arity/type
mismatches; update the code around obj.get_property("length") and the
surrounding function (the function performing the Object-is-function check) to
catch JSException and throw TypeException("Function has incorrect number of
arguments") on error.

448-465: Consider defensive handling in Error accessors.

The message(), stack(), and name() methods assume these properties exist and are strings. While standard JS Error objects have these properties, custom error objects or edge cases could cause TypeException to be thrown from within format().

This is partially mitigated if JSException::dump() validates with JS_IsError() before constructing an Error. However, if Error objects are constructed directly elsewhere, consider adding defensive checks:

♻️ Optional: Safer accessors with fallbacks
 std::string message() const {
-    return this->get_property("message").as<std::string>();
+    auto prop = this->get_optional_property("message");
+    return prop ? prop->to<std::string>().value_or("<no message>") : "<no message>";
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/catter/core/qjs.h` around lines 448 - 465, The Error accessor methods
(message(), stack(), name()) assume the JS properties exist and are strings
which can throw TypeException when used in format(); update these accessors in
qjs.h (the Error class methods message, stack, name and the format() caller) to
defensively check the property before converting: use get_property(...) return
value checks (e.g., verify it's a string or has to_string() safely) or catch
conversion errors and return a sensible fallback (empty string or "<unknown>")
so format() never throws; ensure format() uses the safe accessors rather than
performing raw conversions itself.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/catter/core/js.cc`:
- Around line 106-111: The reject callback created with CallBack::from (the
lambda that sets state = Rejected and appends to error_strace by calling
arg.as<qjs::Error>().format()) needs the same try-catch protection as the
existing catch_fn: wrap the arg.as<qjs::Error>().format() call in a try-catch so
non-Error rejection values (strings, numbers) don't throw TypeException; on
exception, fall back to a safe string representation (e.g., convert arg to
string or use a generic message) and append that to error_strace, mirroring the
catch_fn error-handling pattern.

---

Nitpick comments:
In `@src/catter/core/qjs.h`:
- Around line 1309-1314: The arity check currently calls
obj.get_property("length").as<int64_t>() which can throw JSException; wrap the
property access and conversion in a try/catch that catches JSException (or use a
safe/optional retrieval API if available) and map any failure to the intended
TypeException so callers always get TypeException for arity/type mismatches;
update the code around obj.get_property("length") and the surrounding function
(the function performing the Object-is-function check) to catch JSException and
throw TypeException("Function has incorrect number of arguments") on error.
- Around line 448-465: The Error accessor methods (message(), stack(), name())
assume the JS properties exist and are strings which can throw TypeException
when used in format(); update these accessors in qjs.h (the Error class methods
message, stack, name and the format() caller) to defensively check the property
before converting: use get_property(...) return value checks (e.g., verify it's
a string or has to_string() safely) or catch conversion errors and return a
sensible fallback (empty string or "<unknown>") so format() never throws; ensure
format() uses the safe accessors rather than performing raw conversions itself.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 138d9e15-a5a0-43b7-9a4c-941b0514d397

📥 Commits

Reviewing files that changed from the base of the PR and between 9d80813 and 4a3b903.

📒 Files selected for processing (2)
  • src/catter/core/js.cc
  • src/catter/core/qjs.h

@Seele-Official Seele-Official merged commit 779582d into clice-io:main Mar 18, 2026
5 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Mar 21, 2026
@Seele-Official Seele-Official deleted the qjs-fix branch March 24, 2026 08:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant