Skip to content

util: update isError with new Error.isError #57962

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

miguelmarcondesf
Copy link
Contributor

@miguelmarcondesf miguelmarcondesf commented Apr 21, 2025

Refs 57961
Fixes: #57961

This PR updates the isError function in lib/internal/util.js to use the native Error.isError method if it is available.

@nodejs-github-bot nodejs-github-bot added needs-ci PRs that need a full CI run. util Issues and PRs related to the built-in util module. labels Apr 21, 2025
@@ -95,7 +95,7 @@ function isError(e) {
// An error could be an instance of Error while not being a native error
// or could be from a different realm and not be instance of Error but still
// be a native error.
return isNativeError(e) || e instanceof Error;
return Error.isError ? Error.isError(e) : isNativeError(e) || e instanceof Error;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to ensure backward compatibility?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error.isError should be the entirety of what isNativeError does, and isNativeError should no longer be needed nor instanceof used.

Wouldn't it make more sense to just remove isError entirely, and use the ErrorIsError primordial directly, and add "don't land" labels on this PR?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, @ljharb

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it! Do you think it is okay to continue with this PR and issue or would it be better to create a new one?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine to me to adapt this PR to do that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still want to match errors that are not regular ones. The wild has many examples of these, so we should keep instanceof.

@fisker
Copy link
Contributor

fisker commented Apr 21, 2025

We should deprecate .isError like .isArray

@LiviaMedeiros
Copy link
Member

Just for some clarification:
We have two related functions, util.types.isNativeError() and internalUtil.isError().

The former is exposed to userspace, and is going to be superseded by Error.isError().
Internally we can replace it with ErrorIsError primordial, without fallback and without landing on previous release lines. The only problem is that until V8 update lands on main, there will be no such primordial hence the patched code won't work and pass the CI.
We also can deprecate (starting from doc-only deprecation) and eventually remove this utility. Deprecation before the alternative gets released might look questionable but it's not a hard blocker.

The latter is an internal utility that also checks instanceof Error which can be true even if Error.isError() is not (and vice versa).
Internally we can change or even remove it as long as there's no breaking changes to userspace (or, if there are breaking changes and they are justified, we can do it as semver-major change), but it's likely that simply omitting instanceof check would cause a lot of breakage.

@ljharb
Copy link
Member

ljharb commented Apr 21, 2025

If we have any internal errors that are instanceof Error but not proper errors (such that Error.isError returns false on them) then that's something we should fix asap.

@LiviaMedeiros
Copy link
Member

I believe internalUtil.isError() is used only in assert, inspect, repl, and test_runner. All of these have reasons to expect non-errors with Error in prototype chain sent from wild userspace, and internalUtil.isError() helps with covering this case.
If not, it should be fixed indeed.

@miguelmarcondesf
Copy link
Contributor Author

Just for some clarification: We have two related functions, util.types.isNativeError() and internalUtil.isError().

The former is exposed to userspace, and is going to be superseded by Error.isError(). Internally we can replace it with ErrorIsError primordial, without fallback and without landing on previous release lines. The only problem is that until V8 update lands on main, there will be no such primordial hence the patched code won't work and pass the CI. We also can deprecate (starting from doc-only deprecation) and eventually remove this utility. Deprecation before the alternative gets released might look questionable but it's not a hard blocker.

The latter is an internal utility that also checks instanceof Error which can be true even if Error.isError() is not (and vice versa). Internally we can change or even remove it as long as there's no breaking changes to userspace (or, if there are breaking changes and they are justified, we can do it as semver-major change), but it's likely that simply omitting instanceof check would cause a lot of breakage.

They mentioned in the issue that util.isError is in fact EOL
https://nodejs.org/api/deprecations.html#DEP0048

@miguelmarcondesf
Copy link
Contributor Author

I believe internalUtil.isError() is used only in assert, inspect, repl, and test_runner. All of these have reasons to expect non-errors with Error in prototype chain sent from wild userspace, and internalUtil.isError() helps with covering this case. If not, it should be fixed indeed.

It seems to be also used in comparisons

@miguelmarcondesf
Copy link
Contributor Author

miguelmarcondesf commented Apr 22, 2025

So given all the context, do you think it would be best to just update the doc to use Error.IsError (and not the reference to use Object.prototype.toString(arg) === '[object Error]' || arg instanceof Error) and wait for the new version to arrive to make it easier to find out if there are any cases outside of those mentioned that we expect to be non-errors?

@LiviaMedeiros
Copy link
Member

They mentioned in the issue that util.isError is in fact EOL
https://nodejs.org/api/deprecations.html#DEP0048

util.isError() was part of the public API, which is indeed no longer the case. internalUtil.isError() is an internal helper function that should still exist.
As soon as Error.isError() becomes a thing, we can adjust the implementation of internalUtil.isError() by replacing isNativeError() with ErrorIsError().

So given all the context, do you think it would be best to just update the doc to use Error.IsError (and not the reference to use Object.prototype.toString(arg) === '[object Error]' || arg instanceof Error) and wait for the new version to arrive to make it easier to find out if there are any cases outside of those mentioned that we expect to be non-errors?

We don't need to reference || arg instanceof Error in the docs at all, since we aren't exposing this logic to userland.

I think the best path forward for this PR is to wait for the V8 update to land on main, rebase on it, and then replace all occurrences of isNativeError() with ErrorIsError(). This change would be purely internal: no documentation changes or further justification needed, provided nothing breaks.

Independently, a doc-only deprecation PR for util.types.isNativeError() can be opened.
If you open it before Error.isError() is available, you might need to convince any opponents that deprecating without an alternative is a good idea, and that alternative will behave exactly as isNativeError() does.
Otherwise the deprecation process should be pretty straightforward.

@miguelmarcondesf
Copy link
Contributor Author

miguelmarcondesf commented Apr 22, 2025

Thanks @LiviaMedeiros for all the clarification!

I agree that we should wait for v8 and I'll make the adjustment to all isNativeErrors and I'll also wait to open the issue related to its depreciation, it seemed simpler to me.

@Renegade334
Copy link
Contributor

Independently, a doc-only deprecation PR for util.types.isNativeError() can be opened.

I'm seeing a >50% performance regression for Error.isError over util.types.isNativeError in the current RC.

custom/type-check-nativeerror.js n=1000000 handler="error-iserror" type="native-error": 55,167,312.528783545
custom/type-check-nativeerror.js n=1000000 handler="isnativeerror" type="native-error": 127,300,251.90173846
custom/type-check-nativeerror.js n=1000000 handler="error-iserror" type="native-error-crossrealm": 50,549,863.72514987
custom/type-check-nativeerror.js n=1000000 handler="isnativeerror" type="native-error-crossrealm": 123,061,852.73311144
custom/type-check-nativeerror.js n=1000000 handler="error-iserror" type="non-native-error": 53,863,525.66184942
custom/type-check-nativeerror.js n=1000000 handler="isnativeerror" type="non-native-error": 117,042,527.28524657

I don't think that deprecation should really be considered unless this gets addressed upstream.

As soon as Error.isError() becomes a thing, we can adjust the implementation of internalUtil.isError() by replacing isNativeError() with ErrorIsError().

As a side note, ErrorIsError will not be exposed as a primordial while it remains a flagged feature in V8.

@ljharb
Copy link
Member

ljharb commented May 2, 2025

This PR can't land until it's unflagged, regardless.

@miguelmarcondesf
Copy link
Contributor Author

miguelmarcondesf commented May 7, 2025

Hey, I'm doing some local tests, and indeed, we can't change our internal.isError to use Error.isError because, like @LiviaMedeiros mentioned, we expect some non-errors, and it starts to break a lot of internals like assert. Besides this, I'm not seeing any use of isNativeError outside that context.

Copy link

codecov bot commented May 7, 2025

Codecov Report

Attention: Patch coverage is 0% with 1 line in your changes missing coverage. Please review.

Project coverage is 90.06%. Comparing base (5f252a4) to head (1f56578).
Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
lib/internal/util.js 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #57962      +/-   ##
==========================================
- Coverage   90.12%   90.06%   -0.06%     
==========================================
  Files         629      629              
  Lines      186622   186638      +16     
  Branches    36624    36568      -56     
==========================================
- Hits       168186   168093      -93     
- Misses      11217    11349     +132     
+ Partials     7219     7196      -23     
Files with missing lines Coverage Δ
lib/internal/util.js 95.70% <0.00%> (-0.60%) ⬇️

... and 37 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@miguelmarcondesf
Copy link
Contributor Author

Okay, so keeping e instanceof Error and replacing isNativeError with ErrorisError seems right!
Also, I found other places to replace it, and it seems to be fine as well.

I'll also open another PR to make the deprecation of isNativeError.

@miguelmarcondesf
Copy link
Contributor Author

Independently, a doc-only deprecation PR for util.types.isNativeError() can be opened.

I'm seeing a >50% performance regression for Error.isError over util.types.isNativeError in the current RC.

custom/type-check-nativeerror.js n=1000000 handler="error-iserror" type="native-error": 55,167,312.528783545
custom/type-check-nativeerror.js n=1000000 handler="isnativeerror" type="native-error": 127,300,251.90173846
custom/type-check-nativeerror.js n=1000000 handler="error-iserror" type="native-error-crossrealm": 50,549,863.72514987
custom/type-check-nativeerror.js n=1000000 handler="isnativeerror" type="native-error-crossrealm": 123,061,852.73311144
custom/type-check-nativeerror.js n=1000000 handler="error-iserror" type="non-native-error": 53,863,525.66184942
custom/type-check-nativeerror.js n=1000000 handler="isnativeerror" type="non-native-error": 117,042,527.28524657

I don't think that deprecation should really be considered unless this gets addressed upstream.

As soon as Error.isError() becomes a thing, we can adjust the implementation of internalUtil.isError() by replacing isNativeError() with ErrorIsError().

As a side note, ErrorIsError will not be exposed as a primordial while it remains a flagged feature in V8.

Hey @Renegade334 sorry but what do you mean by "flagged feature in V8"?

@jasnell
Copy link
Member

jasnell commented May 12, 2025

... sorry but what do you mean by "flagged feature in V8"?

The Error.isError API, while enabled by default now in Node 24.0.0 is still only conditionally enabled in v8 using a flag (js-error-iserror), putting it in a special category. When Node.js builds the primordials object the Error.isError(...) function function not actually exist yet on the Error object. It might be a while before it is Just Available without the flag.

@miguelmarcondesf
Copy link
Contributor Author

miguelmarcondesf commented May 12, 2025

... sorry but what do you mean by "flagged feature in V8"?

The Error.isError API, while enabled by default now in Node 24.0.0 is still only conditionally enabled in v8 using a flag (js-error-iserror), putting it in a special category. When Node.js builds the primordials object the Error.isError(...) function function not actually exist yet on the Error object. It might be a while before it is Just Available without the flag.

Thank you for that explanation!
So given this and the possible performance issues, do you think it's best to close these changes for now or wait a little longer?

@geeksilva97
Copy link
Contributor

... sorry but what do you mean by "flagged feature in V8"?

The Error.isError API, while enabled by default now in Node 24.0.0 is still only conditionally enabled in v8 using a flag (js-error-iserror), putting it in a special category. When Node.js builds the primordials object the Error.isError(...) function function not actually exist yet on the Error object. It might be a while before it is Just Available without the flag.

Thank you for that explanation! So given this and the possible performance issues, do you think it's best to close these changes for now or wait a little longer?

We can keep it with a blocked tag since we need to wait for this unflagging

@geeksilva97 geeksilva97 added the blocked PRs that are blocked by other issues or PRs. label May 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blocked PRs that are blocked by other issues or PRs. needs-ci PRs that need a full CI run. util Issues and PRs related to the built-in util module.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Update internal util isError with new Error.isError
9 participants