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
23 changes: 9 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"recommendations": ["justjavac.vscode-deno"]
"recommendations": ["denoland.vscode-deno"]
}
48 changes: 20 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 {
Expand All @@ -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.<anonymous> (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";
Expand All @@ -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)
Expand All @@ -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!
Expand All @@ -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

Expand Down
8 changes: 8 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
23 changes: 23 additions & 0 deletions deno.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 14 additions & 20 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
16 changes: 15 additions & 1 deletion mod_test.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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);
},
});

Expand All @@ -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);
},
});
Loading