Skip to content

Commit

Permalink
fix(async): abortable should prevent uncaught error when promise is…
Browse files Browse the repository at this point in the history
… rejected
  • Loading branch information
Milly committed Dec 28, 2024
1 parent d244cb9 commit 8b1850a
Show file tree
Hide file tree
Showing 2 changed files with 49 additions and 6 deletions.
2 changes: 1 addition & 1 deletion async/abortable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@ function abortablePromise<T>(
p: Promise<T>,
signal: AbortSignal,
): Promise<T> {
if (signal.aborted) return Promise.reject(signal.reason);
const { promise, reject } = Promise.withResolvers<never>();
const abort = () => reject(signal.reason);
if (signal.aborted) abort();
signal.addEventListener("abort", abort, { once: true });
return Promise.race([promise, p]).finally(() => {
signal.removeEventListener("abort", abort);
Expand Down
53 changes: 48 additions & 5 deletions async/abortable_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,26 @@ import { assertEquals, assertRejects } from "@std/assert";
import { abortable } from "./abortable.ts";
import { delay } from "./delay.ts";

Deno.test("abortable() handles promise", async () => {
Deno.test("abortable() handles resolved promise", async () => {
const c = new AbortController();
const { promise, resolve } = Promise.withResolvers<string>();
setTimeout(() => resolve("Hello"), 10);
const result = await abortable(promise, c.signal);
assertEquals(result, "Hello");
});

Deno.test("abortable() handles promise with aborted signal after delay", async () => {
Deno.test("abortable() handles rejected promise", async () => {
const c = new AbortController();
const { promise, reject } = Promise.withResolvers<string>();
setTimeout(() => reject(new Error("This is my error")), 10);
await assertRejects(
() => abortable(promise, c.signal),
Error,
"This is my error",
);
});

Deno.test("abortable() handles resolved promise with aborted signal after delay", async () => {
const c = new AbortController();
const { promise, resolve } = Promise.withResolvers<string>();
setTimeout(() => resolve("Hello"), 10);
Expand All @@ -25,7 +36,22 @@ Deno.test("abortable() handles promise with aborted signal after delay", async (
await delay(5); // wait for the promise to resolve
});

Deno.test("abortable() handles promise with aborted signal after delay with reason", async () => {
Deno.test("abortable() handles rejected promise with aborted signal after delay", async () => {
const c = new AbortController();
const { promise, reject } = Promise.withResolvers<string>();
setTimeout(() => reject(new Error("This is my error")), 10);
setTimeout(() => c.abort(), 5);
const error = await assertRejects(
() => abortable(promise, c.signal),
DOMException,
"The signal has been aborted",
);
assertEquals(error.name, "AbortError");
await delay(5); // wait for the promise to reject
// an uncaught error should not occur
});

Deno.test("abortable() handles resolved promise with aborted signal after delay with reason", async () => {
const c = new AbortController();
const { promise, resolve } = Promise.withResolvers<string>();
setTimeout(() => resolve("Hello"), 10);
Expand All @@ -38,7 +64,7 @@ Deno.test("abortable() handles promise with aborted signal after delay with reas
await delay(5); // wait for the promise to resolve
});

Deno.test("abortable() handles promise with already aborted signal", async () => {
Deno.test("abortable() handles resolved promise with already aborted signal", async () => {
const c = new AbortController();
const { promise, resolve } = Promise.withResolvers<string>();
setTimeout(() => resolve("Hello"), 10);
Expand All @@ -54,7 +80,24 @@ Deno.test("abortable() handles promise with already aborted signal", async () =>
await delay(10); // wait for the promise to resolve
});

Deno.test("abortable() handles promise with already aborted signal with reason", async () => {
Deno.test("abortable() handles rejected promise with already aborted signal", async () => {
const c = new AbortController();
const { promise, reject } = Promise.withResolvers<string>();
setTimeout(() => reject(new Error("This is my error")), 10);
c.abort();
const error = await assertRejects(
async () => {
await abortable(promise, c.signal);
},
DOMException,
"The signal has been aborted",
);
assertEquals(error.name, "AbortError");
await delay(10); // wait for the promise to reject
// an uncaught error should not occur
});

Deno.test("abortable() handles resolved promise with already aborted signal and reason", async () => {
const c = new AbortController();
const { promise, resolve } = Promise.withResolvers<string>();
setTimeout(() => resolve("Hello"), 10);
Expand Down

0 comments on commit 8b1850a

Please sign in to comment.