From 1c6a86dd2c0623ee1af043a8491dbc96d40e883b Mon Sep 17 00:00:00 2001 From: Julian Dax Date: Tue, 11 Nov 2025 18:06:10 +0100 Subject: [PATCH] fix: turn AxiosError into a native error (#5394) (#5558) * fix: turn AxiosError into a native error (#5394) Being an object returned by the 'Error' constructor turns something into a 'native error'. * fix: simplify code in AxiosError * fix: simplify code in AxiosError * refactor: implement AxiosError as a class * refactor: implement CanceledError as a class This turns CanceledError into a native error. * refactor: simplify AxiosError.toJSON * fix: improve error code handling in `AxiosError.from` If no error code is provided, use the code from the underlying error. * fix: set error status in `AxiosError.constructor` If a response is passed to the constructor, set the response status as a property. * fix: remove unnecessary async --------- Co-authored-by: Jay --- lib/cancel/CanceledError.js | 33 +++-- lib/core/AxiosError.js | 163 +++++++++--------------- test/specs/cancel/CanceledError.spec.js | 6 + test/specs/core/AxiosError.spec.js | 13 ++ 4 files changed, 97 insertions(+), 118 deletions(-) diff --git a/lib/cancel/CanceledError.js b/lib/cancel/CanceledError.js index 880066e..e769b89 100644 --- a/lib/cancel/CanceledError.js +++ b/lib/cancel/CanceledError.js @@ -1,25 +1,22 @@ 'use strict'; import AxiosError from '../core/AxiosError.js'; -import utils from '../utils.js'; -/** - * A `CanceledError` is an object that is thrown when an operation is canceled. - * - * @param {string=} message The message. - * @param {Object=} config The config. - * @param {Object=} request The request. - * - * @returns {CanceledError} The created error. - */ -function CanceledError(message, config, request) { - // eslint-disable-next-line no-eq-null,eqeqeq - AxiosError.call(this, message == null ? 'canceled' : message, AxiosError.ERR_CANCELED, config, request); - this.name = 'CanceledError'; +class CanceledError extends AxiosError { + /** + * A `CanceledError` is an object that is thrown when an operation is canceled. + * + * @param {string=} message The message. + * @param {Object=} config The config. + * @param {Object=} request The request. + * + * @returns {CanceledError} The created error. + */ + constructor(message, config, request) { + super(message == null ? 'canceled' : message, AxiosError.ERR_CANCELED, config, request); + this.name = 'CanceledError'; + this.__CANCEL__ = true; + } } -utils.inherits(CanceledError, AxiosError, { - __CANCEL__: true -}); - export default CanceledError; diff --git a/lib/core/AxiosError.js b/lib/core/AxiosError.js index 3d118cb..89de3de 100644 --- a/lib/core/AxiosError.js +++ b/lib/core/AxiosError.js @@ -2,109 +2,72 @@ import utils from '../utils.js'; -/** - * Create an Error with the specified message, config, error code, request and response. - * - * @param {string} message The error message. - * @param {string} [code] The error code (for example, 'ECONNABORTED'). - * @param {Object} [config] The config. - * @param {Object} [request] The request. - * @param {Object} [response] The response. - * - * @returns {Error} The created error. - */ -function AxiosError(message, code, config, request, response) { - Error.call(this); +class AxiosError extends Error { + static from(error, code, config, request, response, customProps) { + const axiosError = new AxiosError(error.message, code || error.code, config, request, response); + axiosError.cause = error; + axiosError.name = error.name; + customProps && Object.assign(axiosError, customProps); + return axiosError; + } - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor); - } else { - this.stack = (new Error()).stack; - } + /** + * Create an Error with the specified message, config, error code, request and response. + * + * @param {string} message The error message. + * @param {string} [code] The error code (for example, 'ECONNABORTED'). + * @param {Object} [config] The config. + * @param {Object} [request] The request. + * @param {Object} [response] The response. + * + * @returns {Error} The created error. + */ + constructor(message, code, config, request, response) { + super(message); + this.name = 'AxiosError'; + this.isAxiosError = true; + code && (this.code = code); + config && (this.config = config); + request && (this.request = request); + if (response) { + this.response = response; + this.status = response.status; + } + } - this.message = message; - this.name = 'AxiosError'; - code && (this.code = code); - config && (this.config = config); - request && (this.request = request); - if (response) { - this.response = response; - this.status = response.status ? response.status : null; - } + toJSON() { + return { + // Standard + message: this.message, + name: this.name, + // Microsoft + description: this.description, + number: this.number, + // Mozilla + fileName: this.fileName, + lineNumber: this.lineNumber, + columnNumber: this.columnNumber, + stack: this.stack, + // Axios + config: utils.toJSONObject(this.config), + code: this.code, + status: this.status, + }; + } } -utils.inherits(AxiosError, Error, { - toJSON: function toJSON() { - return { - // Standard - message: this.message, - name: this.name, - // Microsoft - description: this.description, - number: this.number, - // Mozilla - fileName: this.fileName, - lineNumber: this.lineNumber, - columnNumber: this.columnNumber, - stack: this.stack, - // Axios - config: utils.toJSONObject(this.config), - code: this.code, - status: this.status - }; - } -}); - -const prototype = AxiosError.prototype; -const descriptors = {}; - -[ - 'ERR_BAD_OPTION_VALUE', - 'ERR_BAD_OPTION', - 'ECONNABORTED', - 'ETIMEDOUT', - 'ERR_NETWORK', - 'ERR_FR_TOO_MANY_REDIRECTS', - 'ERR_DEPRECATED', - 'ERR_BAD_RESPONSE', - 'ERR_BAD_REQUEST', - 'ERR_CANCELED', - 'ERR_NOT_SUPPORT', - 'ERR_INVALID_URL' -// eslint-disable-next-line func-names -].forEach(code => { - descriptors[code] = {value: code}; -}); - -Object.defineProperties(AxiosError, descriptors); -Object.defineProperty(prototype, 'isAxiosError', {value: true}); - -// eslint-disable-next-line func-names -AxiosError.from = (error, code, config, request, response, customProps) => { - const axiosError = Object.create(prototype); - - utils.toFlatObject(error, axiosError, function filter(obj) { - return obj !== Error.prototype; - }, prop => { - return prop !== 'isAxiosError'; - }); - - const msg = error && error.message ? error.message : 'Error'; - - // Prefer explicit code; otherwise copy the low-level error's code (e.g. ECONNREFUSED) - const errCode = code == null && error ? error.code : code; - AxiosError.call(axiosError, msg, errCode, config, request, response); - - // Chain the original error on the standard field; non-enumerable to avoid JSON noise - if (error && axiosError.cause == null) { - Object.defineProperty(axiosError, 'cause', { value: error, configurable: true }); - } - - axiosError.name = (error && error.name) || 'Error'; - - customProps && Object.assign(axiosError, customProps); - - return axiosError; -}; +// This can be changed to static properties as soon as the parser options in .eslint.cjs are updated. +AxiosError.ERR_BAD_OPTION_VALUE = 'ERR_BAD_OPTION_VALUE'; +AxiosError.ERR_BAD_OPTION = 'ERR_BAD_OPTION'; +AxiosError.ECONNABORTED = 'ECONNABORTED'; +AxiosError.ETIMEDOUT = 'ETIMEDOUT'; +AxiosError.ERR_NETWORK = 'ERR_NETWORK'; +AxiosError.ERR_FR_TOO_MANY_REDIRECTS = 'ERR_FR_TOO_MANY_REDIRECTS'; +AxiosError.ERR_DEPRECATED = 'ERR_DEPRECATED'; +AxiosError.ERR_BAD_RESPONSE = 'ERR_BAD_RESPONSE'; +AxiosError.ERR_BAD_REQUEST = 'ERR_BAD_REQUEST'; +AxiosError.ERR_CANCELED = 'ERR_CANCELED'; +AxiosError.ERR_NOT_SUPPORT = 'ERR_NOT_SUPPORT'; +AxiosError.ERR_INVALID_URL = 'ERR_INVALID_URL'; export default AxiosError; diff --git a/test/specs/cancel/CanceledError.spec.js b/test/specs/cancel/CanceledError.spec.js index 1061bbe..57839ca 100644 --- a/test/specs/cancel/CanceledError.spec.js +++ b/test/specs/cancel/CanceledError.spec.js @@ -12,4 +12,10 @@ describe('Cancel', function() { expect(cancel.toString()).toBe('CanceledError: Operation has been canceled.'); }); }); + it('should be a native error as checked by the NodeJS `isNativeError` function', function (){ + if((typeof process !== 'undefined') && (process.release.name === 'node')){ + let {isNativeError} = require('node:util/types'); + expect(isNativeError(new CanceledError("My Canceled Error"))).toBeTruthy(); + } + }); }); diff --git a/test/specs/core/AxiosError.spec.js b/test/specs/core/AxiosError.spec.js index ba127d7..3f4f594 100644 --- a/test/specs/core/AxiosError.spec.js +++ b/test/specs/core/AxiosError.spec.js @@ -1,5 +1,6 @@ import AxiosError from '../../../lib/core/AxiosError'; + describe('core::AxiosError', function() { it('should create an Error with message, config, code, request, response, stack and isAxiosError', function() { const request = { path: '/foo' }; @@ -49,6 +50,18 @@ describe('core::AxiosError', function() { }); }); + it('should be a native error as checked by the NodeJS `isNativeError` function', function (){ + if((typeof process !== 'undefined') && (process.release.name === 'node')){ + let {isNativeError} = require('node:util/types'); + expect(isNativeError(new AxiosError("My Axios Error"))).toBeTruthy(); + } + }); + + it('should create an error using one of the static class properties as an error code', function (){ + const myError = new AxiosError("My Axios Error", AxiosError.ECONNABORTED); + expect(myError.code).toEqual(AxiosError.ECONNABORTED); + }); + it('should have status property when response was passed to the constructor', () => { const err = new AxiosError('test', 'foo', {}, {}, {status: 400}); expect(err.status).toBe(400);