diff --git a/src/htmx.js b/src/htmx.js index 7f444c11c..cf2cc5dfe 100644 --- a/src/htmx.js +++ b/src/htmx.js @@ -671,7 +671,9 @@ var htmx = (() => { ? this.parseInterval(ctx.request.timeout) : this.config.defaultTimeout; if (timeout) { - ctx.requestTimeout = setTimeout(() => ctx.request?.abort?.(), timeout); + ctx.requestTimeout = setTimeout(() => { + ctx.request?.abort?.(new DOMException("Request timed out", "TimeoutError")); + }, timeout); } } diff --git a/test/tests/unit/__issueRequest.js b/test/tests/unit/__issueRequest.js index 2da021ad4..b0d21f813 100644 --- a/test/tests/unit/__issueRequest.js +++ b/test/tests/unit/__issueRequest.js @@ -337,6 +337,23 @@ describe('__issueRequest unit tests', function() { await htmx.__issueRequest(ctx) assert.isTrue(errorFired) assert.isTrue(ctx.request.signal.aborted) + assert.equal(ctx.request.signal.reason?.name, 'TimeoutError') + }) + + it('timeout abort reason is distinguishable from hx-sync replace', async function () { + let div = createProcessedHTML('
') + let ctx = htmx.__createRequestContext(div, new Event('click')) + ctx.fetch = (url, opts) => new Promise((_, reject) => { + opts.signal.addEventListener('abort', () => { + reject(opts.signal.reason || new DOMException('The operation was aborted', 'AbortError')) + }) + }) + let p = htmx.__issueRequest(ctx) + await new Promise(r => setTimeout(r, 10)) + ctx.request.abort() + await p + assert.isTrue(ctx.request.signal.aborted) + assert.notEqual(ctx.request.signal.reason?.name, 'TimeoutError') }) it('htmx:abort event aborts in-flight request', async function () { diff --git a/www/src/content/reference/03-events/11-htmx-error.md b/www/src/content/reference/03-events/11-htmx-error.md index 13fd9934a..bacb55295 100644 --- a/www/src/content/reference/03-events/11-htmx-error.md +++ b/www/src/content/reference/03-events/11-htmx-error.md @@ -28,4 +28,6 @@ htmx.on('htmx:error', (evt) => { }); ``` +Timeouts and intentional aborts (`hx-sync` replace/abort, `htmx:abort`) both surface as a failed `fetch()`. Distinguish them with `ctx.request.signal.reason`: a timeout uses `TimeoutError` (`"Request timed out"`); a user or sync abort remains `AbortError`. + Use this for centralized error handling and user feedback.