Versions
Index

preserve-caught-error

Disallow losing originally caught error when re-throwing custom errors

Recommended

Using the recommended config from @eslint/js in a configuration file enables this rule

💡 hasSuggestions

Some problems reported by this rule are manually fixable by editor suggestions

JavaScript developers often re-throw errors in catch blocks to add context but forget to preserve the original error, resulting in lost debugging information.

Using the cause option when throwing new errors helps retain the original error and maintain complete error chains, which improves debuggability and traceability.

try {
	await fetch("https://xyz.com/resource");
} catch(error) {
	// Throw a more specific error without losing original context
	throw new Error("Failed to fetch resource", {
		cause: error
	});
}

Rule Details

This rule enforces the use of the cause property when throwing a new error inside a catch block.

Checks for all built-in error types that support passing a cause.

Examples of incorrect code for this rule:

Open in Playground
/* eslint preserve-caught-error: "error" */

// Not using the `cause` option
try {
    // ...
} catch (error) {
    throw new Error("Something went wrong: " + error.message);
}

// Throwing a new Error with unrelated cause
try {
	doSomething();
} catch (err) {
	const unrelated = new Error("other");
	throw new Error("Something failed", { cause: unrelated });
}

// Caught error is being lost partially due to destructuring
try {
	doSomething();
} catch ({ message, ...rest }) {
	throw new Error(message);
}

// Cause error is being shadowed by a closer scoped redeclaration.
try {
    doSomething();
} catch (error) {
    if (whatever) {
        const error = anotherError; // This declaration is the problem.
        throw new Error("Something went wrong", { cause: error });
    }
}

Examples of correct code for this rule:

Open in Playground
/* eslint preserve-caught-error: "error" */

try {
    // ...
} catch (error) {
    throw new Error("Something went wrong", { cause: error });
}

// When the thrown error is not directly related to the caught error.
try {
} catch (error) {
	foo = {
		bar() {
			// This throw is not directly related to the caught error.
			throw new Error("Something went wrong");
		}
	};
}

// No throw inside catch
try {
    doSomething();
} catch (e) {
    console.error(e);
}

// Ignoring the caught error at the parameter level
// This is valid by default, but this behavior can be changed
// by using the `requireCatchParameter` option discussed below.
try {
	doSomething();
} catch {
	throw new TypeError("Something went wrong");
}

Options

This rule takes a single option — an object with the following optional properties:

  • requireCatchParameter: Requires the catch blocks to always have the caught error parameter when set to true. By default, this is false.
  • errorClassNames: Additional error class names to check for cause preservation. By default, this is [].

requireCatchParameter

Enabling this option mandates for all the catch blocks to have a caught error parameter. This makes sure that the caught error is not discarded at the parameter level.

"preserve-caught-error": ["error", {
  "requireCatchParameter": true
}]

Example of incorrect code for the { "requireCatchParameter": true } option:

Open in Playground
/* eslint preserve-caught-error: ["error", { "requireCatchParameter": true }] */

try {
	doSomething();
} catch { // Can't discard the error ❌
	throw new Error("Something went wrong");
}

Example of correct code for the { "requireCatchParameter": true } option:

Open in Playground
/* eslint preserve-caught-error: ["error", { "requireCatchParameter": true }] */

try {
	doSomething();
} catch(error) { // Error is being referenced ✅
	// Handling and re-throw logic
}

errorClassNames

By default, this rule checks only the built-in Error types (Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, AggregateError). Use errorClassNames to also check custom error classes.

Each entry can be either a string or an object:

  • A string specifies the class name. The constructor is assumed to accept the options object as the second argument, matching the built-in Error signature.
  • An object with name and argumentPosition is used when the constructor accepts the options object at a different position. argumentPosition is 1-indexed.
{
    "rules": {
        "preserve-caught-error": ["error", {
            "errorClassNames": [
                "AppError",
                { "name": "APIError", "argumentPosition": 3 }
            ]
        }]
    }
}

Example of incorrect code for the { "errorClassNames": ["AppError"] } option:

Open in Playground
/* eslint preserve-caught-error: ["error", { "errorClassNames": ["AppError"] }] */

class AppError extends Error {}

try {
	doSomething();
} catch (err) {
	throw new AppError("Something failed");
}

Example of correct code for the { "errorClassNames": ["AppError"] } option:

Open in Playground
/* eslint preserve-caught-error: ["error", { "errorClassNames": ["AppError"] }] */

class AppError extends Error {}

try {
	doSomething();
} catch (err) {
	throw new AppError("Something failed", { cause: err });
}

Example of incorrect code for the { "errorClassNames": [{ "name": "APIError", "argumentPosition": 3 }] } option:

Open in Playground
/* eslint preserve-caught-error: ["error", { "errorClassNames": [{ "name": "APIError", "argumentPosition": 3 }] }] */

class APIError extends Error {
	constructor(message, statusCode, options) {
		super(message, options);
		this.statusCode = statusCode;
	}
}

try {
	doSomething();
} catch (err) {
	throw new APIError("Request failed", 500);
}

Example of correct code for the { "errorClassNames": [{ "name": "APIError", "argumentPosition": 3 }] } option:

Open in Playground
/* eslint preserve-caught-error: ["error", { "errorClassNames": [{ "name": "APIError", "argumentPosition": 3 }] }] */

class APIError extends Error {
	constructor(message, statusCode, options) {
		super(message, options);
		this.statusCode = statusCode;
	}
}

try {
	doSomething();
} catch (err) {
	throw new APIError("Request failed", 500, { cause: err });
}

Known Limitations

The errorClassNames option accepts names, not references to specific error classes. The rule matches these configured names in the AST and does not resolve scope or type information.

As a result, a local class can shadow the intended error class while having the same name. The rule cannot distinguish the local class from the intended class and may report a false positive.

For example:

/* eslint preserve-caught-error: ["error", { errorClassNames: ["AppError"] }] */

function makeWrapped() {
	class AppError {
		constructor(err) {
			this.original = err;
		}
	}

	try {
		doSomething();
	} catch (err) {
		throw new AppError(err);
	}
}

Here, AppError is configured by name, but the local AppError class is different from the intended global/imported class. The rule still matches the name and may report a missing cause, even though this local class has a different constructor signature and does not accept an options object containing cause.

When Not To Use It

You might not want to enable this rule if:

  • You follow a custom error-handling approach where the original error is intentionally omitted from re-thrown errors (e.g., to avoid exposing internal details or to log the original error separately).

  • You use a third-party or internal error-handling library that preserves error context using non-standard properties (e.g., verror) instead of the cause option.

  • (In rare cases) you are targeting legacy environments where the cause option in Error constructors is not supported.

Version

This rule was introduced in ESLint v9.35.0.

Further Reading

Resources

Change Language