diff --git a/lib/events.js b/lib/events.js index 71776c6e8f6f00..6d838d6d112dd9 100644 --- a/lib/events.js +++ b/lib/events.js @@ -810,7 +810,7 @@ async function once(emitter, name, options = {}) { const signal = options?.signal; validateAbortSignal(signal, 'options.signal'); if (signal?.aborted) - throw new AbortError(); + throw new AbortError(undefined, { cause: signal?.reason }); return new Promise((resolve, reject) => { const errorListener = (err) => { emitter.removeListener(name, resolver); @@ -835,7 +835,7 @@ async function once(emitter, name, options = {}) { function abortListener() { eventTargetAgnosticRemoveListener(emitter, name, resolver); eventTargetAgnosticRemoveListener(emitter, 'error', errorListener); - reject(new AbortError()); + reject(new AbortError(undefined, { cause: signal?.reason })); } if (signal != null) { eventTargetAgnosticAddListener( @@ -888,7 +888,7 @@ function on(emitter, event, options) { const signal = options?.signal; validateAbortSignal(signal, 'options.signal'); if (signal?.aborted) - throw new AbortError(); + throw new AbortError(undefined, { cause: signal?.reason }); const unconsumedEvents = []; const unconsumedPromises = []; @@ -976,7 +976,7 @@ function on(emitter, event, options) { return iterator; function abortListener() { - errorHandler(new AbortError()); + errorHandler(new AbortError(undefined, { cause: signal?.reason })); } function eventHandler(...args) { diff --git a/lib/fs.js b/lib/fs.js index 86fb163f2a428c..aa69abb38a29d0 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -350,7 +350,7 @@ function readFileAfterStat(err, stats) { function checkAborted(signal, callback) { if (signal?.aborted) { - callback(new AbortError()); + callback(new AbortError(undefined, { cause: signal?.reason })); return true; } return false; @@ -2050,7 +2050,7 @@ function lutimesSync(path, atime, mtime) { function writeAll(fd, isUserFd, buffer, offset, length, signal, callback) { if (signal?.aborted) { - const abortError = new AbortError(); + const abortError = new AbortError(undefined, { cause: signal?.reason }); if (isUserFd) { callback(abortError); } else { diff --git a/lib/internal/blob.js b/lib/internal/blob.js index 5fb25e04fe1abe..4b4c7b750c4258 100644 --- a/lib/internal/blob.js +++ b/lib/internal/blob.js @@ -297,7 +297,7 @@ class Blob { job.ondone = (err, ab) => { if (err !== undefined) - return reject(new AbortError()); + return reject(new AbortError(undefined, { cause: err })); resolve(ab); }; this[kArrayBufferPromise] = diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 4dadb268cea683..309f6bb3105341 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -821,8 +821,11 @@ function hideInternalStackFrames(error) { // to make usage of the error in userland and readable-stream easier. // It is a regular error with `.code` and `.name`. class AbortError extends Error { - constructor() { - super('The operation was aborted'); + constructor(message = 'The operation was aborted', options = undefined) { + if (options !== undefined && typeof options !== 'object') { + throw new codes.ERR_INVALID_ARG_TYPE('options', 'Object', options); + } + super(message, options); this.code = 'ABORT_ERR'; this.name = 'AbortError'; } diff --git a/lib/internal/fs/promises.js b/lib/internal/fs/promises.js index 5b3482b99b26ee..b92f189c54c1aa 100644 --- a/lib/internal/fs/promises.js +++ b/lib/internal/fs/promises.js @@ -370,7 +370,7 @@ async function fsCall(fn, handle, ...args) { function checkAborted(signal) { if (signal?.aborted) - throw new AbortError(); + throw new AbortError(undefined, { cause: signal?.reason }); } async function writeFileHandle(filehandle, data, signal, encoding) { diff --git a/lib/internal/fs/read_file_context.js b/lib/internal/fs/read_file_context.js index afa4b7852f6790..55ad6d767ecf7d 100644 --- a/lib/internal/fs/read_file_context.js +++ b/lib/internal/fs/read_file_context.js @@ -88,7 +88,8 @@ class ReadFileContext { let length; if (this.signal?.aborted) { - return this.close(new AbortError()); + return this.close( + new AbortError(undefined, { cause: this.signal?.reason })); } if (this.size === 0) { buffer = Buffer.allocUnsafeSlow(kReadFileUnknownBufferLength); diff --git a/lib/internal/fs/watchers.js b/lib/internal/fs/watchers.js index b45af42d12ff7d..f86860f8fc0aed 100644 --- a/lib/internal/fs/watchers.js +++ b/lib/internal/fs/watchers.js @@ -316,13 +316,13 @@ async function* watch(filename, options = {}) { } if (signal?.aborted) - throw new AbortError(); + throw new AbortError(undefined, { cause: signal?.reason }); const handle = new FSEvent(); let { promise, resolve, reject } = createDeferredPromise(); const oncancel = () => { handle.close(); - reject(new AbortError()); + reject(new AbortError(undefined, { cause: signal?.reason })); }; try { @@ -361,7 +361,7 @@ async function* watch(filename, options = {}) { yield await promise; ({ promise, resolve, reject } = createDeferredPromise()); } - throw new AbortError(); + throw new AbortError(undefined, { cause: signal?.reason }); } finally { handle.close(); signal?.removeEventListener('abort', oncancel); diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 473c1244f75da7..de75a7108e9886 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -1778,7 +1778,9 @@ class ClientHttp2Session extends Http2Session { const { signal } = options; if (signal) { validateAbortSignal(signal, 'options.signal'); - const aborter = () => stream.destroy(new AbortError()); + const aborter = () => { + stream.destroy(new AbortError(undefined, { cause: signal.reason })); + }; if (signal.aborted) { aborter(); } else { diff --git a/lib/internal/streams/add-abort-signal.js b/lib/internal/streams/add-abort-signal.js index 80814f0936782d..9bcd202ec63c1e 100644 --- a/lib/internal/streams/add-abort-signal.js +++ b/lib/internal/streams/add-abort-signal.js @@ -34,7 +34,7 @@ module.exports.addAbortSignalNoValidate = function(signal, stream) { return stream; } const onAbort = () => { - stream.destroy(new AbortError()); + stream.destroy(new AbortError(undefined, { cause: signal.reason })); }; if (signal.aborted) { onAbort(); diff --git a/lib/internal/streams/duplexify.js b/lib/internal/streams/duplexify.js index a15d97d36a5f6e..6fb4e35bafa099 100644 --- a/lib/internal/streams/duplexify.js +++ b/lib/internal/streams/duplexify.js @@ -214,7 +214,8 @@ function fromAsyncGen(fn) { const { chunk, done, cb } = await _promise; process.nextTick(cb); if (done) return; - if (signal.aborted) throw new AbortError(); + if (signal.aborted) + throw new AbortError(undefined, { cause: signal.reason }); ({ promise, resolve } = createDeferredPromise()); yield chunk; } diff --git a/lib/internal/streams/end-of-stream.js b/lib/internal/streams/end-of-stream.js index 1fa43e96a01a1e..2fe69207c6a841 100644 --- a/lib/internal/streams/end-of-stream.js +++ b/lib/internal/streams/end-of-stream.js @@ -217,7 +217,9 @@ function eos(stream, options, callback) { // Keep it because cleanup removes it. const endCallback = callback; cleanup(); - endCallback.call(stream, new AbortError()); + endCallback.call( + stream, + new AbortError(undefined, { cause: options.signal.reason })); }; if (options.signal.aborted) { process.nextTick(abort); diff --git a/lib/internal/webstreams/adapters.js b/lib/internal/webstreams/adapters.js index 7718fcc2c6b198..5e01d6eb8eb9dd 100644 --- a/lib/internal/webstreams/adapters.js +++ b/lib/internal/webstreams/adapters.js @@ -122,8 +122,7 @@ function newWritableStreamFromStreamWritable(streamWritable) { const cleanup = finished(streamWritable, (error) => { if (error?.code === 'ERR_STREAM_PREMATURE_CLOSE') { - const err = new AbortError(); - err.cause = error; + const err = new AbortError(undefined, { cause: error }); error = err; } @@ -403,8 +402,7 @@ function newReadableStreamFromStreamReadable(streamReadable) { const cleanup = finished(streamReadable, (error) => { if (error?.code === 'ERR_STREAM_PREMATURE_CLOSE') { - const err = new AbortError(); - err.cause = error; + const err = new AbortError(undefined, { cause: error }); error = err; } diff --git a/lib/readline.js b/lib/readline.js index 759b9ca2469201..11514614c33ab9 100644 --- a/lib/readline.js +++ b/lib/readline.js @@ -157,7 +157,8 @@ Interface.prototype.question[promisify.custom] = function(query, options) { options = typeof options === 'object' && options !== null ? options : {}; if (options.signal && options.signal.aborted) { - return PromiseReject(new AbortError()); + return PromiseReject( + new AbortError(undefined, { cause: options.signal.reason })); } return new Promise((resolve, reject) => { @@ -165,7 +166,7 @@ Interface.prototype.question[promisify.custom] = function(query, options) { if (options.signal) { const onAbort = () => { - reject(new AbortError()); + reject(new AbortError(undefined, { cause: options.signal.reason })); }; options.signal.addEventListener('abort', onAbort, { once: true }); cb = (answer) => { diff --git a/lib/readline/promises.js b/lib/readline/promises.js index 649f92f181758c..534558ec31ffdc 100644 --- a/lib/readline/promises.js +++ b/lib/readline/promises.js @@ -30,12 +30,13 @@ class Interface extends _Interface { if (options?.signal) { validateAbortSignal(options.signal, 'options.signal'); if (options.signal.aborted) { - return reject(new AbortError()); + return reject( + new AbortError(undefined, { cause: options.signal.reason })); } const onAbort = () => { this[kQuestionCancel](); - reject(new AbortError()); + reject(new AbortError(undefined, { cause: options.signal.reason })); }; options.signal.addEventListener('abort', onAbort, { once: true }); cb = (answer) => { diff --git a/lib/timers/promises.js b/lib/timers/promises.js index 162f465da29dec..75b7f443401656 100644 --- a/lib/timers/promises.js +++ b/lib/timers/promises.js @@ -24,10 +24,10 @@ const { validateObject, } = require('internal/validators'); -function cancelListenerHandler(clear, reject) { +function cancelListenerHandler(clear, reject, signal) { if (!this._destroyed) { clear(this); - reject(new AbortError()); + reject(new AbortError(undefined, { cause: signal?.reason })); } } @@ -57,7 +57,7 @@ function setTimeout(after, value, options = {}) { // to 12.x, then this can be converted to use optional chaining to // simplify the check. if (signal && signal.aborted) { - return PromiseReject(new AbortError()); + return PromiseReject(new AbortError(undefined, { cause: signal.reason })); } let oncancel; const ret = new Promise((resolve, reject) => { @@ -66,7 +66,7 @@ function setTimeout(after, value, options = {}) { if (signal) { oncancel = FunctionPrototypeBind(cancelListenerHandler, // eslint-disable-next-line no-undef - timeout, clearTimeout, reject); + timeout, clearTimeout, reject, signal); signal.addEventListener('abort', oncancel); } }); @@ -101,7 +101,7 @@ function setImmediate(value, options = {}) { // to 12.x, then this can be converted to use optional chaining to // simplify the check. if (signal && signal.aborted) { - return PromiseReject(new AbortError()); + return PromiseReject(new AbortError(undefined, { cause: signal.reason })); } let oncancel; const ret = new Promise((resolve, reject) => { @@ -110,7 +110,8 @@ function setImmediate(value, options = {}) { if (signal) { oncancel = FunctionPrototypeBind(cancelListenerHandler, // eslint-disable-next-line no-undef - immediate, clearImmediate, reject); + immediate, clearImmediate, reject, + signal); signal.addEventListener('abort', oncancel); } }); @@ -127,7 +128,7 @@ async function* setInterval(after, value, options = {}) { validateBoolean(ref, 'options.ref'); if (signal?.aborted) - throw new AbortError(); + throw new AbortError(undefined, { cause: signal?.reason }); let onCancel; let interval; @@ -147,7 +148,9 @@ async function* setInterval(after, value, options = {}) { // eslint-disable-next-line no-undef clearInterval(interval); if (callback) { - callback(PromiseReject(new AbortError())); + callback( + PromiseReject( + new AbortError(undefined, { cause: signal.reason }))); callback = undefined; } }; @@ -162,7 +165,7 @@ async function* setInterval(after, value, options = {}) { yield value; } } - throw new AbortError(); + throw new AbortError(undefined, { cause: signal?.reason }); } finally { // eslint-disable-next-line no-undef clearInterval(interval); diff --git a/test/parallel/test-errors-aborterror.js b/test/parallel/test-errors-aborterror.js new file mode 100644 index 00000000000000..15da9f06f94b4d --- /dev/null +++ b/test/parallel/test-errors-aborterror.js @@ -0,0 +1,31 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const { + strictEqual, + throws, +} = require('assert'); +const { AbortError } = require('internal/errors'); + +{ + const err = new AbortError(); + strictEqual(err.message, 'The operation was aborted'); + strictEqual(err.cause, undefined); +} + +{ + const cause = new Error('boom'); + const err = new AbortError('bang', { cause }); + strictEqual(err.message, 'bang'); + strictEqual(err.cause, cause); +} + +{ + throws(() => new AbortError('', false), { + code: 'ERR_INVALID_ARG_TYPE' + }); + throws(() => new AbortError('', ''), { + code: 'ERR_INVALID_ARG_TYPE' + }); +} diff --git a/test/parallel/test-readline-promises-interface.js b/test/parallel/test-readline-promises-interface.js index e137886fa19690..e4bf483c3dc851 100644 --- a/test/parallel/test-readline-promises-interface.js +++ b/test/parallel/test-readline-promises-interface.js @@ -910,6 +910,15 @@ for (let i = 0; i < 12; i++) { rli.close(); } + (async () => { + const [rli] = getInterface({ terminal }); + const signal = AbortSignal.abort('boom'); + await assert.rejects(rli.question('hello', { signal }), { + cause: 'boom', + }); + rli.close(); + })().then(common.mustCall()); + // Throw an error when question is executed with an aborted signal { const ac = new AbortController(); diff --git a/test/parallel/test-timers-immediate-promisified.js b/test/parallel/test-timers-immediate-promisified.js index 65c8411f1b2ffd..5808312b564e48 100644 --- a/test/parallel/test-timers-immediate-promisified.js +++ b/test/parallel/test-timers-immediate-promisified.js @@ -97,3 +97,10 @@ process.on('multipleResolves', common.mustNotCall()); assert.strictEqual(stderr, ''); })); } + +(async () => { + const signal = AbortSignal.abort('boom'); + await assert.rejects(timerPromises.setImmediate(undefined, { signal }), { + cause: 'boom', + }); +})().then(common.mustCall()); diff --git a/test/parallel/test-timers-interval-promisified.js b/test/parallel/test-timers-interval-promisified.js index 28a0d0b47cd182..9c11a9d9870fa7 100644 --- a/test/parallel/test-timers-interval-promisified.js +++ b/test/parallel/test-timers-interval-promisified.js @@ -246,3 +246,15 @@ process.on('multipleResolves', common.mustNotCall()); setPromiseTimeout(time_unit * 3).then(() => post = true), ]).then(common.mustCall()); } + +(async () => { + const signal = AbortSignal.abort('boom'); + try { + const iterable = timerPromises.setInterval(2, undefined, { signal }); + // eslint-disable-next-line no-unused-vars + for await (const _ of iterable) {} + assert.fail('should have failed'); + } catch (err) { + assert.strictEqual(err.cause, 'boom'); + } +})().then(common.mustCall()); diff --git a/test/parallel/test-timers-timeout-promisified.js b/test/parallel/test-timers-timeout-promisified.js index 0b9a6b6f19a1c2..4e3881acec1ce2 100644 --- a/test/parallel/test-timers-timeout-promisified.js +++ b/test/parallel/test-timers-timeout-promisified.js @@ -97,3 +97,10 @@ process.on('multipleResolves', common.mustNotCall()); assert.strictEqual(stderr, ''); })); } + +(async () => { + const signal = AbortSignal.abort('boom'); + await assert.rejects(timerPromises.setTimeout(1, undefined, { signal }), { + cause: 'boom', + }); +})().then(common.mustCall());