◆ abort メソッドに引数を渡せない
◆ abort に理由を渡せるような fetch ラッパーを作った……けどできることは制限される

abort

fetch は AbortController を使ってキャンセルできます

const abort_controller = new AbortController()
setTimeout(() => abort_controller.abort(), 2000)
try {
const response = await fetch("http://unknown-domain/", { signal: abort_controller.signal })
} catch(err) {
console.log(err)
}
DOMException

AbortController のインスタンスを作ると signal と abort プロパティがあります
signal プロパティを fetch のオプションとして渡し abort 関数を呼び出すと signal を渡した fetch がキャンセルされます
ちょっと扱いづらいですが互換性を考えると fetch の返り値を Promise から変えられませんし 返り値のメソッドを使ってキャンセルができないなら キャンセルを伝えるためのなにかを引数として渡しておくしかありません

signal は EventTarget を継承したオブジェクトで addEventListener や dispatchEvent メソッドがあります
abort 関数の実行で abort イベントが起きるというものです
ただ 他のイベントと違って 一度 abort を実行すると 2 回目はイベントが発生しません
dispatchEvent を使った場合は イベントは起きます

const ac = new AbortController()
ac.signal.addEventListener("abort", console.log)

ac.abort()
// Event {isTrusted: true, type: "abort", target: AbortSignal, currentTarget: AbortSignal, eventPhase: 2, …}

ac.abort()
// (何も表示されない)

ac.signal.dispatchEvent(new Event("abort"))
// Event {isTrusted: false, type: "abort", target: AbortSignal, currentTarget: AbortSignal, eventPhase: 2, …}

fetch の内部ではこのイベントを検知してキャンセル処理が行われるようです

abort エラーの原因

キャンセルされると fetch はエラーになるので catch で error オブジェクトを受け取れます
型は DOMException というもので中身はこういう感じです

{
code: 20
message: "The user aborted a request."
name: "AbortError"
}

name や code でキャンセルされたんだと判定できます
ただ code は非推奨なので name で判定したほうがいいです
https://developer.mozilla.org/en-US/docs/Web/API/DOMException

JavaScript では数値のコードより名前の文字列を使うことが多いですね
attachShadow の mode だったり キーボードイベントの key や code だったり (数値が入ってる keyCode は deprecated)
扱いや速度が対して変わらないならそれだけじゃ意味がわからない数値よりも文字列のほうが便利だからでしょうか
個人的にはビット演算でフラグの組み合わせでもしない限り 定数を使って数値として扱うのは好きじゃないのでこっちのほうが嬉しいです

判定できると言っても これは 「abort された」 ということまでです
abort 理由がユーザがキャンセルボタンを押したからなのかタイムアウトなのかなどの情報はありません
abort の引数に reason を入れられるのかと思ったのに仕様を見ると引数は無しのようです

理由を設定できるように

abort に引数を渡せないし abort 関数の呼び出しによって起こる abort イベントの中にもそれらしいデータはありません
このイベントは fetch の catch 部分では受け取れず DOMException だけなので 自分で追加しても無意味です

仕方ないので使い方は限定されるもののキャンセル理由を渡せる fetch を作りました

function fetch2(url, opt) {
const abort_controller = new AbortController()
const response = fetch(url, { ...opt, signal: abort_controller.signal })
let abort_reason
return {
response: response.catch(err => {
if (err.name === "AbortError") {
err.abort_reason = abort_reason
}
throw err
}),
abort(reason) {
abort_reason = reason
abort_controller.abort()
},
get abort_reason() {
return abort_reason
},
}
}
{
const { response, abort } = fetch2(".")
const res = await response
console.log(res.status)
// 200
}
{
const { response, abort } = fetch2("http://unknown-domain/")
setTimeout(() => abort("TIMEOUT"), 1000)
try {
const res = await response
} catch (err) {
console.log(err.abort_reason)
}
// TIMEOUT
}

AbortController を内部で毎回作っていて abort と response を返します
自動でそれぞれの fetch に対して作られるので 1 つの signal を複数の fetch に設定みたいな高度なことはできません
返す abort 関数では引数を受け取り 内部の変数に保存します
response の catch も内部で行い AbortError が原因のエラーの場合はエラーオブジェクトに abort_reason プロパティを追加して再度 throw します

途中でタイムアウト

多くの場合はレスポンスを作ってしまってから まとめて返すので await response が成功したら response.text() などの返り値もすぐに取得できます
ですが大きなサイズのレスポンスだったり Node.js で

res.write("A")
await sleep1minute()
res.end("B")

みたいな処理をすると response が届いてから本文が全て届くまでに時間がかかります
こうなると text などの response のメソッドの途中でタイムアウトの可能性でてきます
上の fetch2 では response のエラーのみ catch していて text や json メソッドの返り値の Promise が reject された場合の処理はしていません
やろうとするとこれらのメソッドをラップすることになって面倒です
最初から fetchJSON や fetchText みたいにしてしまうことも考えましたが response から status code を使う可能性もありますし fetch2 の返り値のオブジェクトの abort_reason プロパティでエラーを取得できるようにしました
AbortError の場合にはここをみることで原因を取得できます
response と text で見る場所が変わるので catch して AbortError の DOMException にプロパティ追加もしないほうがいいかもしれませんが 基本キャンセルするのって response までだと思うので よく使うところは便利に使いたいので入れておきます