Repository: unjs/h3 (ts)

Issue:
Option to provide non-wrapped version of error (onError).
### Describe the feature

Hi,

I've been implementing a custom error handler using `onError` and it works fine, except for one part.  Since every error is wrapped using H3Error class, code like this isn't possible:

```ts
class CustomError extends Error {}

const app = createApp({
  onError(error, event) {
    if (error instanceof CustomError) {
      // ..
    }
  },
});
```

This would be super useful in case of custom validation error handling, for example, `ZodError` class. The key part missing is `instanceof` since each error is wrapped using H3 (https://github.com/unjs/h3/blob/main/src/error.ts#L55).

I was wondering, is there interest in providing an option to `AppOptions` for "vanilla" error classes?

### Additional information

- [X] Would you be willing to help implement this feature?

Pull request description:
feat: preserve and make error cause accessable
<!---
☝️ PR title should follow conventional commits (https://conventionalcommits.org) 
-->

### 🔗 Linked issue

Resolves #377

### ❓ Type of change

<!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->

- [ ] 📖 Documentation (updates to the documentation, readme, or JSdoc annotations)
- [ ] 🐞 Bug fix (a non-breaking change that fixes an issue)
- [x] 👌 Enhancement (improving an existing functionality like performance)
- [ ] ✨ New feature (a non-breaking change that adds functionality)
- [ ] 🧹 Chore (updates to the build process or auxiliary tools and libraries)
- [ ] ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

### 📚 Description

This PR improves H3Error class with a polyfill to support [v8 error cause](https://v8.dev/features/error-cause) universally. 

The `createError` wrapper also always provides input (or `input.cause` if provided) as cause so we can always access original error/object.

Another small fix: `onError` types fixed. It accepts an H3Error.

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->
<!-- If your change requires a documentation PR, please link it appropriately -->
<!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [ ] I have linked an issue or discussion.
- [ ] I have updated the documentation accordingly.


Patch:
diff --git a/src/app.ts b/src/app.ts
index 53fabe7..97dc842 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -6,7 +6,7 @@ import {
   eventHandler,
   H3Event,
 } from "./event";
-import { createError } from "./error";
+import { H3Error, createError } from "./error";
 import {
   send,
   sendStream,
@@ -48,7 +48,7 @@ export interface AppUse {
 
 export interface AppOptions {
   debug?: boolean;
-  onError?: (error: Error, event: H3Event) => any;
+  onError?: (error: H3Error, event: H3Event) => any;
 }
 
 export interface App {
diff --git a/src/error.ts b/src/error.ts
index 0070130..9ed2728 100644
--- a/src/error.ts
+++ b/src/error.ts
@@ -25,6 +25,17 @@ export class H3Error extends Error {
   unhandled = false;
   statusMessage?: string;
   data?: any;
+  cause?: unknown;
+
+  constructor(message: string, opts: { cause?: unknown } = {}) {
+    // @ts-ignore https://v8.dev/features/error-cause
+    super(message, opts);
+
+    // Polyfill cause for other runtimes
+    if (opts.cause && !this.cause) {
+      this.cause = opts.cause;
+    }
+  }
 
   toJSON() {
     const obj: Pick<
@@ -63,11 +74,9 @@ export function createError(
     return input;
   }
 
-  const err = new H3Error(
-    input.message ?? input.statusMessage ?? "",
-    // @ts-ignore https://v8.dev/features/error-cause
-    input.cause ? { cause: input.cause } : undefined
-  );
+  const err = new H3Error(input.message ?? input.statusMessage ?? "", {
+    cause: input.cause || input,
+  });
 
   if ("stack" in input) {
     try {


Test patch:
diff --git a/test/error.test.ts b/test/error.test.ts
index 7fdd082..cbfbd39 100644
--- a/test/error.test.ts
+++ b/test/error.test.ts
@@ -1,11 +1,12 @@
 import supertest, { SuperTest, Test } from "supertest";
-import { describe, it, expect, beforeEach, vi } from "vitest";
+import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
 import {
   createApp,
   App,
   createError,
   toNodeListener,
   eventHandler,
+  H3Error,
 } from "../src";
 
 const consoleMock = ((global.console.error as any) = vi.fn());
@@ -14,11 +15,22 @@ describe("error", () => {
   let app: App;
   let request: SuperTest<Test>;
 
+  const capturedErrors: H3Error[] = [];
+
   beforeEach(() => {
-    app = createApp({ debug: false });
+    app = createApp({
+      debug: false,
+      onError(error) {
+        capturedErrors.push(error);
+      },
+    });
     request = supertest(toNodeListener(app));
   });
 
+  afterEach(() => {
+    capturedErrors.length = 0;
+  });
+
   it("logs errors", async () => {
     app.use(
       eventHandler(() => {
@@ -119,4 +131,22 @@ describe("error", () => {
     const res = await request.get("/");
     expect(res.status).toBe(501);
   });
+
+  it("can access original error", async () => {
+    class CustomError extends Error {
+      customError: true;
+    }
+
+    app.use(
+      "/",
+      eventHandler(() => {
+        throw createError(new CustomError());
+      })
+    );
+
+    const res = await request.get("/");
+    expect(res.status).toBe(500);
+
+    expect(capturedErrors[0].cause).toBeInstanceOf(CustomError);
+  });
 });