Skip to content
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

fix(async): abortable should prevent uncaught error when promise is rejected #6312

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
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
91 changes: 66 additions & 25 deletions async/abortable_test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,89 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
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>();
const t = setTimeout(() => resolve("Hello"), 100);
setTimeout(() => resolve("Hello"), 10);
const result = await abortable(promise, c.signal);
assertEquals(result, "Hello");
clearTimeout(t);
});

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>();
const t = setTimeout(() => resolve("Hello"), 100);
setTimeout(() => c.abort(), 50);
setTimeout(() => resolve("Hello"), 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 resolve
});

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");
clearTimeout(t);
await delay(5); // wait for the promise to reject
// an uncaught error should not occur
});

Deno.test("abortable() handles promise with aborted signal after delay with reason", async () => {
Deno.test("abortable() handles resolved promise with aborted signal after delay with reason", async () => {
const c = new AbortController();
const { promise, resolve } = Promise.withResolvers<string>();
const t = setTimeout(() => resolve("Hello"), 100);
setTimeout(() => c.abort(new Error("This is my reason")), 50);
setTimeout(() => resolve("Hello"), 10);
setTimeout(() => c.abort(new Error("This is my reason")), 5);
await assertRejects(
() => abortable(promise, c.signal),
Error,
"This is my reason",
);
clearTimeout(t);
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>();
const t = setTimeout(() => resolve("Hello"), 100);
setTimeout(() => resolve("Hello"), 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 resolve
});

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 () => {
Expand All @@ -51,46 +93,46 @@ Deno.test("abortable() handles promise with already aborted signal", async () =>
"The signal has been aborted",
);
assertEquals(error.name, "AbortError");
clearTimeout(t);
await delay(10); // wait for the promise to reject
// an uncaught error should not occur
});

Deno.test("abortable() handles promise with already aborted signal with reason", async () => {
Deno.test("abortable() handles resolved promise with already aborted signal and reason", async () => {
const c = new AbortController();
const { promise, resolve } = Promise.withResolvers<string>();
const t = setTimeout(() => resolve("Hello"), 100);
setTimeout(() => resolve("Hello"), 10);
c.abort(new Error("This is my reason"));
await assertRejects(
() => abortable(promise, c.signal),
Error,
"This is my reason",
);
clearTimeout(t);
await delay(10); // wait for the promise to resolve
});

Deno.test("abortable.AsyncIterable()", async () => {
const c = new AbortController();
const { promise, resolve } = Promise.withResolvers<string>();
const t = setTimeout(() => resolve("Hello"), 100);
setTimeout(() => resolve("My promise resolved"), 10);
const a = async function* () {
yield "Hello";
await promise;
yield "World";
};
const items = await Array.fromAsync(abortable(a(), c.signal));
assertEquals(items, ["Hello", "World"]);
clearTimeout(t);
});

Deno.test("abortable.AsyncIterable() handles aborted signal after delay", async () => {
const c = new AbortController();
const { promise, resolve } = Promise.withResolvers<string>();
const t = setTimeout(() => resolve("Hello"), 100);
setTimeout(() => resolve("My promise resolved"), 10);
const a = async function* () {
yield "Hello";
await promise;
yield "World";
};
setTimeout(() => c.abort(), 50);
setTimeout(() => c.abort(), 5);
const items: string[] = [];
const error = await assertRejects(
async () => {
Expand All @@ -103,13 +145,12 @@ Deno.test("abortable.AsyncIterable() handles aborted signal after delay", async
);
assertEquals(error.name, "AbortError");
assertEquals(items, ["Hello"]);
clearTimeout(t);
});

Deno.test("abortable.AsyncIterable() handles already aborted signal", async () => {
const c = new AbortController();
const { promise, resolve } = Promise.withResolvers<string>();
const t = setTimeout(() => resolve("Hello"), 100);
setTimeout(() => resolve("My promise resolved"), 10);
const a = async function* () {
yield "Hello";
await promise;
Expand All @@ -128,7 +169,7 @@ Deno.test("abortable.AsyncIterable() handles already aborted signal", async () =
);
assertEquals(error.name, "AbortError");
assertEquals(items, []);
clearTimeout(t);
await delay(10); // wait for the promise to resolve
});

Deno.test("abortable.AsyncIterable() calls return before throwing", async () => {
Expand All @@ -146,6 +187,7 @@ Deno.test("abortable.AsyncIterable() calls return before throwing", async () =>
}),
return: () => {
returnCalled = true;
clearTimeout(timeoutId);
return Promise.resolve({ value: undefined, done: true });
},
}),
Expand All @@ -164,7 +206,6 @@ Deno.test("abortable.AsyncIterable() calls return before throwing", async () =>
assertEquals(returnCalled, true);
assertEquals(error.name, "AbortError");
assertEquals(items, []);
clearTimeout(timeoutId!);
});

Deno.test("abortable.AsyncIterable() behaves just like original when not aborted", async () => {
Expand Down
Loading