diff --git a/build/tasks/npmcopy.js b/build/tasks/npmcopy.js
index 9bd3eea4..47c943ab 100644
--- a/build/tasks/npmcopy.js
+++ b/build/tasks/npmcopy.js
@@ -8,7 +8,10 @@ const files = {
"qunit/qunit.js": "qunit/qunit/qunit.js",
"qunit/qunit.css": "qunit/qunit/qunit.css",
- "qunit/LICENSE.txt": "qunit/LICENSE.txt"
+ "qunit/LICENSE.txt": "qunit/LICENSE.txt",
+
+ "sinon/sinon.js": "sinon/pkg/sinon.js",
+ "sinon/LICENSE.txt": "sinon/LICENSE"
};
async function npmcopy() {
diff --git a/eslint.config.js b/eslint.config.js
index 00c9b5db..ad9e3590 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -97,6 +97,7 @@ export default [
Symbol: false,
jQuery: false,
QUnit: false,
+ sinon: false,
url: false,
expectWarning: false,
expectNoWarning: false,
diff --git a/package-lock.json b/package-lock.json
index 3a8b22c4..ea1d18ab 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -30,6 +30,7 @@
"qunit": "2.21.0",
"rollup": "4.22.4",
"selenium-webdriver": "4.21.0",
+ "sinon": "9.2.4",
"uglify-js": "3.9.4",
"yargs": "17.7.2"
},
@@ -430,6 +431,45 @@
"win32"
]
},
+ "node_modules/@sinonjs/commons": {
+ "version": "1.8.6",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz",
+ "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz",
+ "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^1.7.0"
+ }
+ },
+ "node_modules/@sinonjs/samsam": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.3.1.tgz",
+ "integrity": "sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^1.6.0",
+ "lodash.get": "^4.4.2",
+ "type-detect": "^4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/text-encoding": {
+ "version": "0.7.3",
+ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz",
+ "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==",
+ "dev": true,
+ "license": "(Unlicense OR Apache-2.0)"
+ },
"node_modules/@types/body-parser": {
"version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@@ -3032,6 +3072,13 @@
"setimmediate": "^1.0.5"
}
},
+ "node_modules/just-extend": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
+ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3078,6 +3125,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -3198,6 +3252,37 @@
"node": ">= 0.6"
}
},
+ "node_modules/nise": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-4.1.0.tgz",
+ "integrity": "sha512-eQMEmGN/8arp0xsvGoQ+B1qvSkR73B1nWSCh7nOt5neMCtwcQVYQGdzQMhcNscktTsWB54xnlSQFzOAPJD8nXA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^1.7.0",
+ "@sinonjs/fake-timers": "^6.0.0",
+ "@sinonjs/text-encoding": "^0.7.1",
+ "just-extend": "^4.0.2",
+ "path-to-regexp": "^1.7.0"
+ }
+ },
+ "node_modules/nise/node_modules/isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nise/node_modules/path-to-regexp": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz",
+ "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "isarray": "0.0.1"
+ }
+ },
"node_modules/node-watch": {
"version": "0.7.3",
"resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.3.tgz",
@@ -4011,6 +4096,49 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/sinon": {
+ "version": "9.2.4",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.4.tgz",
+ "integrity": "sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==",
+ "deprecated": "16.1.1",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^1.8.1",
+ "@sinonjs/fake-timers": "^6.0.1",
+ "@sinonjs/samsam": "^5.3.1",
+ "diff": "^4.0.2",
+ "nise": "^4.0.4",
+ "supports-color": "^7.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/sinon"
+ }
+ },
+ "node_modules/sinon/node_modules/diff": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+ "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/sinon/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/spawnback": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/spawnback/-/spawnback-1.0.1.tgz",
@@ -4301,6 +4429,16 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
diff --git a/package.json b/package.json
index 78fbe0d2..037cadbe 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
"qunit": "2.21.0",
"rollup": "4.22.4",
"selenium-webdriver": "4.21.0",
+ "sinon": "9.2.4",
"uglify-js": "3.9.4",
"yargs": "17.7.2"
},
diff --git a/src/jquery/deferred.js b/src/jquery/deferred.js
index 30268adb..7f036efe 100644
--- a/src/jquery/deferred.js
+++ b/src/jquery/deferred.js
@@ -1,9 +1,15 @@
-import { migratePatchFunc, migratePatchAndWarnFunc } from "../main.js";
+import {
+ migratePatchFunc,
+ migratePatchAndWarnFunc,
+ migrateWarn
+} from "../main.js";
+import { jQueryVersionSince } from "../compareVersions.js";
// Support jQuery slim which excludes the deferred module in jQuery 4.0+
if ( jQuery.Deferred ) {
-var oldDeferred = jQuery.Deferred,
+var unpatchedGetStackHookValue,
+ oldDeferred = jQuery.Deferred,
tuples = [
// Action, add listener, callbacks, .then handlers, final state
@@ -63,4 +69,39 @@ migratePatchFunc( jQuery, "Deferred", function( func ) {
// Preserve handler of uncaught exceptions in promise chains
jQuery.Deferred.exceptionHook = oldDeferred.exceptionHook;
+// Preserve the optional hook to record the error, if defined
+jQuery.Deferred.getErrorHook = oldDeferred.getErrorHook;
+
+// We want to mirror jQuery.Deferred.getErrorHook here, so we cannot use
+// existing Migrate utils.
+Object.defineProperty( jQuery.Deferred, "getStackHook", {
+ configurable: true,
+ enumerable: true,
+ get: function() {
+ if ( jQuery.migrateIsPatchEnabled( "deferred-getStackHook" ) ) {
+
+ // jQuery 3.x checks `getStackHook` if `getErrorHook` missing;
+ // don't warn on the getter there.
+ if ( jQueryVersionSince( "4.0.0" ) ) {
+ migrateWarn( "deferred-getStackHook",
+ "jQuery.Deferred.getStackHook is deprecated; " +
+ "use jQuery.Deferred.getErrorHook" );
+ }
+ return jQuery.Deferred.getErrorHook;
+ } else {
+ return unpatchedGetStackHookValue;
+ }
+ },
+ set: function( newValue ) {
+ if ( jQuery.migrateIsPatchEnabled( "deferred-getStackHook" ) ) {
+ migrateWarn( "deferred-getStackHook",
+ "jQuery.Deferred.getStackHook is deprecated; " +
+ "use jQuery.Deferred.getErrorHook" );
+ jQuery.Deferred.getErrorHook = newValue;
+ } else {
+ unpatchedGetStackHookValue = newValue;
+ }
+ }
+} );
+
}
diff --git a/test/index.html b/test/index.html
index f6086625..18acaddb 100644
--- a/test/index.html
+++ b/test/index.html
@@ -10,6 +10,7 @@
+
diff --git a/test/unit/jquery/deferred.js b/test/unit/jquery/deferred.js
index e602e075..0c91dfd0 100644
--- a/test/unit/jquery/deferred.js
+++ b/test/unit/jquery/deferred.js
@@ -1,7 +1,15 @@
// Support jQuery slim which excludes the deferred module in jQuery 4.0+
if ( jQuery.Deferred ) {
-QUnit.module( "deferred" );
+QUnit.module( "deferred", {
+ beforeEach: function() {
+ this.sandbox = sinon.createSandbox();
+ },
+ afterEach: function() {
+ this.sandbox.restore();
+ jQuery.Deferred.getErrorHook = jQuery.Deferred.getStackHook = undefined;
+ }
+} );
QUnit.test( "jQuery.Deferred.exceptionHook", function( assert ) {
assert.expect( 1 );
@@ -10,6 +18,238 @@ QUnit.test( "jQuery.Deferred.exceptionHook", function( assert ) {
assert.ok( typeof jQuery.Deferred.exceptionHook === "function", "hook is present" );
} );
+QUnit.test( "jQuery.Deferred.getStackHook - getter", function( assert ) {
+ assert.expect( 5 );
+
+ var exceptionHookSpy,
+ done = assert.async();
+
+ // Source: https://github.com/dmethvin/jquery-deferred-reporter
+ function getErrorHook() {
+
+ // Throw an error as IE doesn't capture `stack` of non-thrown ones.
+ try {
+ throw new Error( "Test exception in jQuery.Deferred" );
+ } catch ( err ) {
+ return err;
+ }
+ }
+
+ jQuery.Deferred.getErrorHook = getErrorHook;
+
+ exceptionHookSpy = this.sandbox.spy( jQuery.Deferred, "exceptionHook" );
+
+ expectWarning( assert, "jQuery.Deferred.getStackHook - getter",
+
+ // The getter only warns in jQuery 4+ as jQuery 3.x reads it internally.
+ jQueryVersionSince( "4.0.0" ) ? 1 : 0,
+ function() {
+ assert.strictEqual( jQuery.Deferred.getStackHook, jQuery.Deferred.getErrorHook,
+ "getStackHook mirrors getErrorHook (getter)" );
+ } );
+
+ expectNoWarning( assert, "asyncHook reported in jQuery.Deferred.exceptionHook", function() {
+ jQuery
+ .when()
+ .then( function() {
+ throw new ReferenceError( "Test ReferenceError" );
+ } )
+ .catch( function() {
+ var asyncError = exceptionHookSpy.lastCall.args[ 1 ];
+ assert.ok( asyncError instanceof Error,
+ "Error passed to exceptionHook (instance)" );
+ assert.strictEqual( asyncError.message, "Test exception in jQuery.Deferred",
+ "Error passed to exceptionHook (message)" );
+ done();
+ } );
+ } );
+} );
+
+QUnit.test( "jQuery.Deferred.getStackHook - getter, no getErrorHook", function( assert ) {
+ assert.expect( 1 );
+
+ var done = assert.async();
+
+ expectNoWarning( assert, "No Migrate warning in a regular `then`", function() {
+ jQuery
+ .when()
+ .then( function() {
+ done();
+ } );
+ } );
+} );
+
+QUnit.test( "jQuery.Deferred.getStackHook - setter", function( assert ) {
+ assert.expect( 5 );
+
+ var exceptionHookSpy,
+ done = assert.async();
+
+ exceptionHookSpy = this.sandbox.spy( jQuery.Deferred, "exceptionHook" );
+
+ expectWarning( assert, "jQuery.Deferred.getStackHook - setter", 1, function() {
+ var mockFn = function() {};
+ jQuery.Deferred.getStackHook = mockFn;
+ assert.strictEqual( jQuery.Deferred.getErrorHook, mockFn,
+ "getStackHook mirrors getErrorHook (setter)" );
+ } );
+
+ expectWarning( assert, "asyncHook from jQuery.Deferred.getStackHook reported",
+ 1, function() {
+ jQuery.Deferred.getStackHook = function() {
+
+ // Throw an error as IE doesn't capture `stack` of non-thrown ones.
+ try {
+ throw new SyntaxError( "Different exception in jQuery.Deferred" );
+ } catch ( err ) {
+ return err;
+ }
+ };
+
+ jQuery
+ .when()
+ .then( function() {
+ throw new ReferenceError( "Test ReferenceError" );
+ } )
+ .catch( function() {
+ var asyncError = exceptionHookSpy.lastCall.args[ 1 ];
+ assert.ok( asyncError instanceof SyntaxError,
+ "Error passed to exceptionHook (instance)" );
+ assert.strictEqual( asyncError.message, "Different exception in jQuery.Deferred",
+ "Error passed to exceptionHook (message)" );
+
+ done();
+ } );
+ } );
+} );
+
+QUnit.test( "jQuery.Deferred.getStackHook - disabled patch, getter", function( assert ) {
+ assert.expect( 5 );
+
+ var exceptionHookSpy,
+ done = assert.async();
+
+ // Source: https://github.com/dmethvin/jquery-deferred-reporter
+ function getErrorHook() {
+
+ // Throw an error as IE doesn't capture `stack` of non-thrown ones.
+ try {
+ throw new Error( "Test exception in jQuery.Deferred" );
+ } catch ( err ) {
+ return err;
+ }
+ }
+
+ jQuery.migrateDisablePatches( "deferred-getStackHook" );
+
+ jQuery.Deferred.getErrorHook = getErrorHook;
+
+ exceptionHookSpy = this.sandbox.spy( jQuery.Deferred, "exceptionHook" );
+
+ expectNoWarning( assert, "jQuery.Deferred.getStackHook - getter", function() {
+ assert.strictEqual( jQuery.Deferred.getStackHook, undefined,
+ "getStackHook does not mirror getErrorHook (getter)" );
+ } );
+
+ expectNoWarning( assert, "asyncHook reported in jQuery.Deferred.exceptionHook", function() {
+ jQuery
+ .when()
+ .then( function() {
+ throw new ReferenceError( "Test ReferenceError" );
+ } )
+ .catch( function() {
+ var asyncError = exceptionHookSpy.lastCall.args[ 1 ];
+ assert.ok( asyncError instanceof Error,
+ "Error passed to exceptionHook (instance)" );
+ assert.strictEqual( asyncError.message, "Test exception in jQuery.Deferred",
+ "Error passed to exceptionHook (message)" );
+ done();
+ } );
+ } );
+} );
+
+QUnit.test( "jQuery.Deferred.getStackHook - disabled patch, setter", function( assert ) {
+ assert.expect( jQueryVersionSince( "4.0.0" ) ? 4 : 5 );
+
+ var exceptionHookSpy,
+ done = assert.async();
+
+ // Source: https://github.com/dmethvin/jquery-deferred-reporter
+ function getErrorHook() {
+
+ // Throw an error as IE doesn't capture `stack` of non-thrown ones.
+ try {
+ throw new Error( "Test exception in jQuery.Deferred" );
+ } catch ( err ) {
+ return err;
+ }
+ }
+
+ jQuery.migrateDisablePatches( "deferred-getStackHook" );
+
+ jQuery.Deferred.getErrorHook = getErrorHook;
+
+ exceptionHookSpy = this.sandbox.spy( jQuery.Deferred, "exceptionHook" );
+
+ expectNoWarning( assert, "jQuery.Deferred.getStackHook - setter", function() {
+ var mockFn = function() {};
+ jQuery.Deferred.getStackHook = mockFn;
+ assert.strictEqual( jQuery.Deferred.getErrorHook, getErrorHook,
+ "getStackHook does not mirror getErrorHook (setter)" );
+ } );
+
+ expectNoWarning( assert, "asyncHook from jQuery.Deferred.getStackHook reported", function() {
+ jQuery.Deferred.getErrorHook = undefined;
+ jQuery.Deferred.getStackHook = function() {
+
+ // Throw an error as IE doesn't capture `stack` of non-thrown ones.
+ try {
+ throw new SyntaxError( "Different exception in jQuery.Deferred" );
+ } catch ( err ) {
+ return err;
+ }
+ };
+
+ jQuery
+ .when()
+ .then( function() {
+ throw new ReferenceError( "Test ReferenceError" );
+ } )
+ .catch( function() {
+ var asyncError = exceptionHookSpy.lastCall.args[ 1 ];
+
+ if ( jQueryVersionSince( "4.0.0" ) ) {
+ assert.strictEqual( asyncError, undefined,
+ "Error not passed to exceptionHook" );
+ } else {
+ assert.ok( asyncError instanceof Error,
+ "Error passed to exceptionHook (instance)" );
+ assert.strictEqual( asyncError.message,
+ "Different exception in jQuery.Deferred",
+ "Error passed to exceptionHook (message)" );
+ }
+
+ done();
+ } );
+ } );
+} );
+
+QUnit.test( "jQuery.Deferred.getStackHook - disabled patch, getter + setter interaction",
+ function( assert ) {
+ assert.expect( 3 );
+
+ jQuery.migrateDisablePatches( "deferred-getStackHook" );
+
+ expectNoWarning( assert, "jQuery.Deferred.getStackHook - setter & getter", function() {
+ var mockFn = function() {};
+ assert.strictEqual( jQuery.Deferred.getStackHook, undefined,
+ "getStackHook is `undefined` by default" );
+ jQuery.Deferred.getStackHook = mockFn;
+ assert.strictEqual( jQuery.Deferred.getStackHook, mockFn,
+ "getStackHook getter reports what the setter set" );
+ } );
+} );
+
QUnit.test( ".pipe() warnings", function( assert ) {
assert.expect( 4 );
diff --git a/warnings.md b/warnings.md
index 5908faff..63a33df2 100644
--- a/warnings.md
+++ b/warnings.md
@@ -302,3 +302,9 @@ See jQuery-ui [commit](https://github.com/jquery/jquery-ui/commit/c0093b599fcd58
**Cause:** `jQuery.ajax` calls with `dataType: 'json'` with a provided callback are automatically converted by jQuery to JSONP requests unless the options also specify `jsonp: false`. Auto-promoting JSON requests to JSONP introduces a security risk as the developer may be unaware they're not just downloading data but executing code from a remote domain. This auto-promoting behavior is deprecated and will be removed in jQuery 4.0.0.
**Solution:** To trigger a JSONP request, specify the `dataType: "jsonp"` option.
+
+### \[deferred-getStackHook\] JQMIGRATE: jQuery.Deferred.getStackHook is deprecated; use jQuery.Deferred.getErrorHook
+
+**Cause:** `jQuery.Deferred.getStackHook` was originally created to pass the stack trace from before an async barrier to report when a user error (like calling a non-existing function) causes a promise to be rejected. However, passing a stack trace doesn't take source maps into account, so we started advising to pass the whole error object. To make it clearer, we also renamed the API to `jQuery.Deferred.getErrorHook`. The legacy alias will be removed in jQuery 4.0.0
+
+**Solution:** Rename all usage of `jQuery.Deferred.getStackHook` to `jQuery.Deferred.getErrorHook`. If you previously assigned a function returning an error stack to `jQuery.Deferred.getStackHook` or `jQuery.Deferred.getErrorHook`, change it to return a full error object.