# Error Handling Guide

This document is for Rong module developers, covering error handling patterns at the Rust ↔ JS boundary.

---

## Quick Reference

### Common Scenarios

| Scenario                 | Rust Code                                                                          |
| :---                     | :---                                                                               |
| Type mismatch            | `HostError::new(E_TYPE, "expected string").with_name("TypeError")`                 |
| Wrong argument count     | `HostError::invalid_arg_count(2, got)`                                             |
| Out of range             | `HostError::new(E_OUT_OF_RANGE, "index out of bounds").with_name("RangeError")`    |
| IO failure               | `HostError::new(E_IO, err.to_string())`                                            |
| Permission denied        | `HostError::new(E_PERMISSION_DENIED, "access denied")`                             |
| Operation aborted        | `HostError::aborted(reason)`                                                       |
| Invalid state            | `HostError::new(E_INVALID_STATE, "stream already closed")`                         |
| Preserve JS thrown value | `RongJSError::from_thrown_value(js_value)`                                         |

### Complete Examples

```rust
use rong::error::{E_INVALID_ARG, E_IO, E_TYPE, HostError, JSResult};

// Example 1: Throw TypeError (argument validation)
fn parse_url(url: String) -> JSResult<Url> {
    Url::parse(&url).map_err(|e| {
        HostError::new(E_INVALID_ARG, format!("Invalid URL: {e}"))
            .with_name("TypeError")
            .into()
    })
}

// Example 2: Throw Error with structured data
fn read_file(path: &str) -> JSResult<Vec<u8>> {
    std::fs::read(path).map_err(|e| {
        HostError::new(E_IO, e.to_string())
            .with_data(rong::err_data!({
                path: path,
                os_error: (e.raw_os_error())  // Option<i32> -> number | null
            }))
            .into()
    })
}

// Example 3: Preserve JS thrown value (for callback/promise)
fn call_js_callback(func: JSFunc, args: impl IntoArgs) -> JSResult<JSValue> {
    let result = func.call(None, args)?;
    if result.is_exception() {
        return Err(RongJSError::from_thrown_value(result));
    }
    Ok(result)
}
```

### What JS Receives

```js
try {
    await readFile("/etc/shadow");
} catch (e) {
    e.name;    // "Error"
    e.message; // "permission denied"
    e.code;    // "E_PERMISSION_DENIED" (stable, for programmatic handling)
    e.data;    // { path: "/etc/shadow" } (optional structured details)
}
```

---

## Three Core Rules

### 1. Rust `Err` = JS throw/reject

This is a hard rule, no exceptions:

```
JSResult<T> = Ok(v)  →  JS normal return / Promise.resolve(v)
JSResult<T> = Err(e) →  JS throw / Promise.reject(payload)
```

If you want certain failures to **not enter catch**, you must use `Ok(...)` to return an explicit result object (see "API Design" section).

### 2. Use `is_exception()` to detect exceptions

```rust
// ✅ Correct
if value.is_exception() { return Err(...); }

// ❌ Wrong - is_error() only means "this is an Error object", not an exception
if value.is_exception() || value.is_error() { ... }
```

In JS, `return new Error("x")` is a normal return value; only `throw` enters the exception channel.

### 3. Host failures use HostError, JS thrown values use from_thrown_value

| Error Source      | Rust Representation                 | Use Case                                   |
| :---              | :---                                | :---                                       |
| Rust-side failure | `HostError::new(code, msg)`         | Argument validation, IO, permissions, etc. |
| JS-side throw     | `RongJSError::from_thrown_value(v)` | Exceptions from eval/call/promise          |

---

## Error Code Reference

Location: [`rong::error::E_*`](../core/src/error.rs)

| Code                      | Meaning                   | Typical `.with_name()`        |
| :---                      | :---                      | :---                          |
| `E_TYPE`                  | Type mismatch             | `TypeError`                   |
| `E_INVALID_ARG`           | Invalid argument          | `TypeError`                   |
| `E_OUT_OF_RANGE`          | Index/value out of bounds | `RangeError`                  |
| `E_MISSING_PROPERTY`      | Property not found        | `ReferenceError`              |
| `E_IO`                    | IO error                  | `Error`                       |
| `E_PERMISSION_DENIED`     | Permission denied         | `Error`                       |
| `E_NOT_FOUND`             | Resource not found        | `Error`                       |
| `E_ALREADY_EXISTS`        | Resource already exists   | `Error`                       |
| `E_ABORT`                 | Operation aborted         | `AbortError`                  |
| `E_INVALID_STATE`         | Invalid state             | `Error` / `InvalidStateError` |
| `E_INVALID_DATA`          | Data format error         | `Error`                       |
| `E_NOT_SUPPORTED`         | Feature not supported     | `Error`                       |
| `E_STREAM`                | Stream error              | `Error`                       |
| `E_COMPILE`               | Compilation error         | `SyntaxError`                 |
| `E_INTERNAL`              | Internal error            | `Error`                       |
| `E_ILLEGAL_CONSTRUCTOR`   | Illegal constructor       | `TypeError`                   |
| `E_JS_THROWN`             | Wrapped JS thrown value   | (preserves original name)     |

**Module-specific codes**: Modules can define their own codes with prefixes like `FS_*`, `NET_*`, `HTTP_*`.

---

## API Design: Throwing vs No-throw

### Throwing API (Default)

Failures enter `catch`, suitable for network/IO/permissions/cancellation:

```rust
// Rust
pub async fn fetch(url: String) -> JSResult<Response> { ... }
```

```js
// JS
try {
    const resp = await fetch(url);
} catch (e) {
    // Network failure, cancellation, permission error...
}
```

### No-throw API (Explicit Result)

Failures don't enter `catch`, suitable for frequent expected failures:

```rust
// Rust - note: returns Ok(ResultObject) instead of Err(...)
pub fn try_parse_json(ctx: &JSContext, text: String) -> JSResult<JSValue> {
    match serde_json::from_str::<serde_json::Value>(&text) {
        Ok(v) => Ok(rong::js_value!(ctx, { ok: true, value: (v) })),
        Err(e) => Ok(rong::js_value!(ctx, { ok: false, error: (e.to_string()) })),
    }
}
```

```js
// JS
const result = tryParseJson(text);
if (result.ok) {
    console.log(result.value);
} else {
    console.log("parse failed:", result.error);
}
```

**Naming conventions**: `tryXxx`, `xxxOrNull`, `parseXxxSafe`

---

## Abort Semantics

`AbortController.abort(reason)` must preserve `reason` as-is:

```rust
// ✅ Preserve original reason
if let Some(reason) = signal.reason() {
    return Err(RongJSError::from_thrown_value(reason));
}

// ✅ Use default AbortError when no reason
return Err(HostError::aborted(None).into());
```

```js
// JS expectation
controller.abort("custom reason");
// catch should receive "custom reason" (===), not a wrapped Error
```

---

## HostError API Reference

```rust
// Basic construction
HostError::new(code, message)

// Set JS Error name (TypeError, RangeError, AbortError, ...)
.with_name("TypeError")

// Attach structured data
.with_data(rong::err_data!({ key: "value" }))

// Convenience constructors
HostError::invalid_arg_count(expected, got)  // Wrong argument count
HostError::aborted(reason)                    // Operation aborted

// Convert to RongJSError
host_error.into()  // converts HostError into RongJSError
```

---

## Common Mistakes

### ❌ Using `is_error()` to detect exceptions

```rust
// Wrong: `return new Error("x")` will be treated as exception
if v.is_error() { return Err(...); }
```

### ❌ Losing the JS thrown value

```rust
// Wrong: original thrown value lost, JS can't access cause
Err(HostError::new(E_ERROR, "call failed").into())

// Correct: preserve original value
Err(RongJSError::from_thrown_value(js_exception))
```

### ❌ Mixing throw and no-throw in one function

```rust
// Wrong: sometimes throws, sometimes returns null
pub fn get_item(key: String) -> JSResult<Option<String>> {
    if key.is_empty() {
        return Err(...);  // throws
    }
    Ok(storage.get(&key))  // returns None if not found, no throw
}
```

Pick one style: either always throw, or provide a `tryGetItem` variant.
