Skip to content

feat(assert/unstable): allow asserting predicates against thrown/rejected errors #6629

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
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
6 changes: 6 additions & 0 deletions assert/deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@
"./equal": "./equal.ts",
"./fail": "./fail.ts",
"./unimplemented": "./unimplemented.ts",
"./unstable-is-error": "./unstable_is_error.ts",
"./unstable-is-error-test": "./unstable_is_error_test.ts",
"./unstable-rejects": "./unstable_rejects.ts",
"./unstable-rejects-test": "./unstable_rejects_test.ts",
"./unstable-throws": "./unstable_throws.ts",
"./unstable-throws-test": "./unstable_throws_test.ts",
"./unreachable": "./unreachable.ts"
}
}
82 changes: 82 additions & 0 deletions assert/unstable_is_error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// This module is browser compatible.
import { AssertionError } from "./assertion_error.ts";
import { stripAnsiCode } from "@std/internal/styles";

/**
* A predicate to be checked against the error:
* - If a string is supplied, this must be present in the error's `message` property.
* - If a RegExp is supplied, this must match against the error's `message` property.
* - If a predicate function is provided, this must return `true` for the error.
*/
export type ErrorPredicate<E extends Error> =
| string
| RegExp
| ((e: E) => boolean);

/**
* Make an assertion that `error` is an `Error`.
* If not then an error will be thrown.
* An error class and a string that should be included in the
* error message can also be asserted.
*
* @example Usage
* ```ts ignore
* import { assertIsError } from "@std/assert";
*
* assertIsError(null); // Throws
* assertIsError(new RangeError("Out of range")); // Doesn't throw
* assertIsError(new RangeError("Out of range"), SyntaxError); // Throws
* assertIsError(new RangeError("Out of range"), RangeError, "Out of range"); // Doesn't throw
* assertIsError(new RangeError("Out of range"), RangeError, "Within range"); // Throws
* ```
*
* @typeParam E The type of the error to assert.
* @param error The error to assert.
* @param ErrorClass The optional error class to assert.
* @param predicate An optional string or RegExp to match against the error message, or a callback that should return `true` for the error.
* @param msg The optional message to display if the assertion fails.
*/
export function assertIsError<E extends Error = Error>(
error: unknown,
// deno-lint-ignore no-explicit-any
ErrorClass?: abstract new (...args: any[]) => E,
predicate?: ErrorPredicate<E>,
msg?: string,
): asserts error is E {
const msgSuffix = msg ? `: ${msg}` : ".";

if (!(error instanceof Error)) {
throw new AssertionError(
`Expected "error" to be an Error object${msgSuffix}`,
);
}
if (ErrorClass && !(error instanceof ErrorClass)) {
msg =
`Expected error to be instance of "${ErrorClass.name}", but was "${error?.constructor?.name}"${msgSuffix}`;
throw new AssertionError(msg);
}
let msgCheck;
if (typeof predicate === "string") {
msgCheck = stripAnsiCode(error.message).includes(
stripAnsiCode(predicate),
);
} else if (predicate instanceof RegExp) {
msgCheck = predicate.test(stripAnsiCode(error.message));
} else if (typeof predicate === "function") {
msgCheck = predicate(error as E);
if (!msgCheck) {
msg = `Error failed the check${msgSuffix}`;
throw new AssertionError(msg);
}
}

if (predicate && !msgCheck) {
msg = `Expected error message to ${
predicate instanceof RegExp
? `match ${predicate}`
: `include ${JSON.stringify(predicate)}`
}, but got ${JSON.stringify(error?.message)}${msgSuffix}`;
throw new AssertionError(msg);
}
}
110 changes: 110 additions & 0 deletions assert/unstable_is_error_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { AssertionError, assertThrows } from "./mod.ts";
import { assertIsError } from "./unstable_is_error.ts";

class CustomError extends Error {}
class AnotherCustomError extends Error {}

Deno.test("assertIsError() throws when given value isn't error", () => {
assertThrows(
() => assertIsError("Panic!", undefined, "Panic!"),
AssertionError,
`Expected "error" to be an Error object.`,
);

assertThrows(
() => assertIsError(null),
AssertionError,
`Expected "error" to be an Error object.`,
);

assertThrows(
() => assertIsError(undefined),
AssertionError,
`Expected "error" to be an Error object.`,
);
});

Deno.test("assertIsError() allows subclass of Error", () => {
assertIsError(new AssertionError("Fail!"), Error, "Fail!");
});

Deno.test("assertIsError() allows custom error", () => {
assertIsError(new CustomError("failed"), CustomError, "fail");
assertThrows(
() => assertIsError(new AnotherCustomError("failed"), CustomError, "fail"),
AssertionError,
'Expected error to be instance of "CustomError", but was "AnotherCustomError".',
);
});

Deno.test("assertIsError() accepts abstract class", () => {
abstract class AbstractError extends Error {}
class ConcreteError extends AbstractError {}

assertIsError(new ConcreteError("failed"), AbstractError, "fail");
});

Deno.test("assertIsError() throws with message diff containing double quotes", () => {
assertThrows(
() =>
assertIsError(
new CustomError('error with "double quotes"'),
CustomError,
'doesn\'t include "this message"',
),
AssertionError,
`Expected error message to include "doesn't include \\"this message\\"", but got "error with \\"double quotes\\"".`,
);
});

Deno.test("assertIsError() throws when given value doesn't match regex ", () => {
assertIsError(new AssertionError("Regex test"), Error, /ege/);
assertThrows(
() => assertIsError(new AssertionError("Regex test"), Error, /egg/),
Error,
`Expected error message to match /egg/, but got "Regex test"`,
);
});

Deno.test("assertIsError() throws with custom message", () => {
assertThrows(
() =>
assertIsError(
new CustomError("failed"),
AnotherCustomError,
"fail",
"CUSTOM MESSAGE",
),
AssertionError,
'Expected error to be instance of "AnotherCustomError", but was "CustomError": CUSTOM MESSAGE',
);
});

Deno.test("assertIsError() with custom error check", () => {
class CustomError extends Error {
readonly code: number;
constructor(code: number) {
super();
this.code = code;
}
}

assertIsError(
new CustomError(-1),
CustomError,
(e) => e.code === -1,
);

assertThrows(
() => {
assertIsError(
new CustomError(-1),
CustomError,
(e) => e.code === -2,
);
},
AssertionError,
"Error failed the check.",
);
});
123 changes: 123 additions & 0 deletions assert/unstable_rejects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2018-2025 the Deno authors. MIT license.
// This module is browser compatible.
import { assertIsError, type ErrorPredicate } from "./unstable_is_error.ts";
import { AssertionError } from "./assertion_error.ts";

/**
* Executes a function which returns a promise, expecting it to reject.
*
* To assert that a synchronous function throws, use {@linkcode assertThrows}.
*
* @example Usage
* ```ts ignore
* import { assertRejects } from "@std/assert";
*
* await assertRejects(async () => Promise.reject(new Error())); // Doesn't throw
* await assertRejects(async () => console.log("Hello world")); // Throws
* ```
*
* @param fn The function to execute.
* @param msg The optional message to display if the assertion fails.
* @returns The promise which resolves to the thrown error.
*/
export function assertRejects(
fn: () => PromiseLike<unknown>,
msg?: string,
): Promise<unknown>;
/**
* Executes a function which returns a promise, expecting it to reject.
* If it does not, then it throws. An error class and a string that should be
* included in the error message can also be asserted.
*
* To assert that a synchronous function throws, use {@linkcode assertThrows}.
*
* @example Usage
* ```ts ignore
* import { assertRejects } from "@std/assert";
*
* await assertRejects(async () => Promise.reject(new Error()), Error); // Doesn't throw
* await assertRejects(async () => Promise.reject(new Error()), SyntaxError); // Throws
* ```
*
* @typeParam E The error class to assert.
* @param fn The function to execute.
* @param ErrorClass The error class to assert.
* @param predicate An optional string or RegExp to match against the error message, or a callback that should return `true` for the error.
* @param msg The optional message to display if the assertion fails.
* @returns The promise which resolves to the thrown error.
*/
export function assertRejects<E extends Error = Error>(
fn: () => PromiseLike<unknown>,
// deno-lint-ignore no-explicit-any
ErrorClass: abstract new (...args: any[]) => E,
predicate?: ErrorPredicate<E>,
msg?: string,
): Promise<E>;
export async function assertRejects<E extends Error = Error>(
fn: () => PromiseLike<unknown>,
errorClassOrMsg?:
// deno-lint-ignore no-explicit-any
| (abstract new (...args: any[]) => E)
| string,
predicate?: ErrorPredicate<E>,
msg?: string,
): Promise<E | Error | unknown> {
// deno-lint-ignore no-explicit-any
let ErrorClass: (abstract new (...args: any[]) => E) | undefined;
let err;

if (typeof errorClassOrMsg !== "string") {
if (
errorClassOrMsg === undefined ||
errorClassOrMsg.prototype instanceof Error ||
errorClassOrMsg.prototype === Error.prototype
) {
ErrorClass = errorClassOrMsg;
} else {
msg = predicate as string;
}

Check warning on line 78 in assert/unstable_rejects.ts

View check run for this annotation

Codecov / codecov/patch

assert/unstable_rejects.ts#L77-L78

Added lines #L77 - L78 were not covered by tests
} else {
msg = errorClassOrMsg;
}
let doesThrow = false;
let isPromiseReturned = false;
const msgSuffix = msg ? `: ${msg}` : ".";
try {
const possiblePromise = fn();
if (
possiblePromise &&
typeof possiblePromise === "object" &&
typeof possiblePromise.then === "function"
) {
isPromiseReturned = true;
await possiblePromise;
} else {
throw new Error();
}
} catch (error) {
if (!isPromiseReturned) {
throw new AssertionError(
`Function throws when expected to reject${msgSuffix}`,
);
}
if (ErrorClass) {
if (!(error instanceof Error)) {
throw new AssertionError(`A non-Error object was rejected${msgSuffix}`);
}
assertIsError(
error,
ErrorClass,
predicate,
msg,
);
}
err = error;
doesThrow = true;
}
if (!doesThrow) {
throw new AssertionError(
`Expected function to reject${msgSuffix}`,
);
}
return err;
}
Loading
Loading