diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b085aa..60abd34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,20 +3,15 @@ name: ci on: [push, pull_request] jobs: - build: - name: run test with deno version ${{ matrix.deno }} + check: + name: check runs-on: ubuntu-latest - strategy: - matrix: - deno: [0.40.0] steps: - - name: clone repo - uses: actions/checkout@v1 - - name: install deno - uses: denolib/setup-deno@master + - name: Clone repo + uses: actions/checkout@v4 + - name: Install Deno + uses: denoland/setup-deno@v2 with: - deno-version: ${{ matrix.deno }} - - name: run fmt - run: deno fmt --check - - name: run tests - run: deno test + deno-version: v2.x + - name: Run checks + run: deno task check diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 370e8da..74baffc 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,3 +1,3 @@ { - "recommendations": ["justjavac.vscode-deno"] + "recommendations": ["denoland.vscode-deno"] } diff --git a/README.md b/README.md index 30b5f64..213c1b8 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,16 @@ [![tag](https://img.shields.io/github/release/denomod/user_error)](https://github.com/denomod/user_error/releases) [![Build Status](https://github.com/denomod/user_error/workflows/ci/badge.svg?branch=master)](https://github.com/denomod/user_error/actions) [![license](https://img.shields.io/github/license/denomod/user_error)](https://github.com/denomod/user_error/blob/master/LICENSE) -[![](https://img.shields.io/badge/deno-v0.26.0-green.svg)](https://github.com/denoland/deno) +[![](https://img.shields.io/badge/deno-v2.x-green.svg)](https://github.com/denoland/deno) -UserError is a base class that makes JavaScript errors a lot more useful. -It gives you: +UserError is a base class that makes JavaScript errors a lot more useful. It +gives you: -- An `Error` subclass that actually works +- A small `Error` base class for custom errors - Support for checking error types using `instanceof` -- A correct `name` property on error objects +- A correct subclass `name` property on error objects - Cleaner stack traces +- Native `ErrorOptions`, including `cause` ## Usage @@ -27,7 +28,8 @@ class MyError extends UserError { ## Rationale -To see the problems UserError solves for you, let's try to subclass `Error` directly and see what happens. +Modern JavaScript runtimes support subclassing `Error`, but custom error classes +still need boilerplate to set the visible error name. ```ts class MyError extends Error { @@ -43,34 +45,24 @@ const boom = (): never => { try { boom(); } catch (error) { - error instanceof Error; // true (correct) - error instanceof MyError; // false (wrong, should be true) + error instanceof Error; // true + error instanceof MyError; // true error.name; // Error (wrong, should be MyError) } ``` -In this example, subclassing `Error` is useless; -we can't really differentiate an instance of `MyError` from any other `Error` in the system. -This inability to subclass `Error` has led to a number of other workarounds, -most often adding some kind of `code` property to error objects, -so you end up doing stuff like this: - -```ts -if (error.code === SomeErrorCode) - // ... -``` - -In addition to this problem, errors created in this way include extra noise in their stack trace: +You can set `this.name = "MyError"` in every custom error class, but that is +easy to forget and easy to get out of sync when classes are renamed. If you rely +on the default name, logs and serialized errors lose the custom type: ```log Error: boom! << should be "MyError: boom!" - at MyError.Error (native) << noise - at new MyError (test.js:3:7) << noise at boom (test.js:10:9) at Object. (test.js:14:3) ``` -UserError aims to fix these problems. Now, when we run the example it looks like this: +UserError centralizes that setup. When we run the example with UserError it +looks like this: ```ts import UserError from "https://deno.land/x/user_error/mod.ts"; @@ -94,7 +86,7 @@ try { } ``` -Since both `instanceof` work and the `name` property is setup correctly, we can do either +Since both `instanceof` and the `name` property work, you can do either ```ts if (error instanceof MyError) @@ -108,8 +100,8 @@ if (error.name === 'MyError') // ... ``` -instead of duck-typing with a custom property. -Additionally, the stack trace doesn't contain unnecessary entries: +instead of duck-typing with a custom property. Additionally, the stack trace +doesn't contain unnecessary entries: ```log MyError: boom! @@ -119,8 +111,8 @@ MyError: boom! ### License -[user_error](https://github.com/denomod/user_error) is released under the MIT License. -See the bundled [LICENSE](./LICENSE) file for details. +[user_error](https://github.com/denomod/user_error) is released under the MIT +License. See the bundled [LICENSE](./LICENSE) file for details. ## Thanks diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..8718d3e --- /dev/null +++ b/deno.json @@ -0,0 +1,8 @@ +{ + "tasks": { + "check": "deno fmt --check && deno lint && deno check mod.ts mod_test.ts && deno test" + }, + "imports": { + "@std/assert": "jsr:@std/assert@^1.0.14" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..98f6af0 --- /dev/null +++ b/deno.lock @@ -0,0 +1,23 @@ +{ + "version": "5", + "specifiers": { + "jsr:@std/assert@^1.0.14": "1.0.19", + "jsr:@std/internal@^1.0.12": "1.0.14" + }, + "jsr": { + "@std/assert@1.0.19": { + "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e", + "dependencies": [ + "jsr:@std/internal" + ] + }, + "@std/internal@1.0.14": { + "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7" + } + }, + "workspace": { + "dependencies": [ + "jsr:@std/assert@^1.0.14" + ] + } +} diff --git a/mod.ts b/mod.ts index 9c3e19d..f1e62d6 100644 --- a/mod.ts +++ b/mod.ts @@ -1,23 +1,17 @@ -function UserError(this: Error, message: string) { - Error.call(this); +type StackTraceErrorConstructor = ErrorConstructor & { + captureStackTrace?: ( + targetObject: object, + constructorOpt?: typeof UserError, + ) => void; +}; - if ((Error as any).captureStackTrace) { - (Error as any).captureStackTrace(this, this.constructor); - } - - this.name = this.constructor.name; - this.message = message; -} +export default class UserError extends Error { + constructor(message?: string, options?: ErrorOptions) { + super(message, options); -UserError.prototype = Object.create(Error.prototype, { - constructor: { - value: UserError, - configurable: true, - enumerable: false, - writable: true, - }, -}); + this.name = new.target.name; -Object.setPrototypeOf(UserError, Error); - -export default UserError as ErrorConstructor; + const ErrorWithStackTrace = Error as StackTraceErrorConstructor; + ErrorWithStackTrace.captureStackTrace?.(this, new.target); + } +} diff --git a/mod_test.ts b/mod_test.ts index f143b3e..2971ca9 100644 --- a/mod_test.ts +++ b/mod_test.ts @@ -1,4 +1,4 @@ -import { assertEquals } from "https://deno.land/std/testing/asserts.ts"; +import { assertEquals, assertInstanceOf } from "@std/assert"; import UserError from "./mod.ts"; @@ -8,6 +8,8 @@ Deno.test({ const error = new UserError("Bang!"); assertEquals(error.name, "UserError"); assertEquals(error.message, "Bang!"); + assertInstanceOf(error, Error); + assertInstanceOf(error, UserError); }, }); @@ -22,5 +24,17 @@ Deno.test({ const error = new MyError("Bang!"); assertEquals(error.name, "MyError"); assertEquals(error.message, "Bang!"); + assertInstanceOf(error, Error); + assertInstanceOf(error, UserError); + assertInstanceOf(error, MyError); + }, +}); + +Deno.test({ + name: "test UserError cause", + fn(): void { + const cause = new Error("Cause"); + const error = new UserError("Bang!", { cause }); + assertEquals(error.cause, cause); }, });