Skip to content
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

Deep equality check for Errors #1065

Closed
janis91 opened this issue Oct 19, 2017 · 16 comments
Closed

Deep equality check for Errors #1065

janis91 opened this issue Oct 19, 2017 · 16 comments

Comments

@janis91
Copy link

janis91 commented Oct 19, 2017

Hi,
because we use chai and chai-as-promised in our project, we recently tried to use .rejectedWith() in order to check for a custom error:

class HttpError extends Error {
    constructor(statusCode, message) {
        super(message);
        this.statusCode = statusCode;
        Object.setPrototypeOf(this, new.target.prototype);
        this.name = new.target.name;
    }
    toString() {
        return `${this.name}(${this.statusCode}): ${this.message}`;
    }
}

But the following check doesn't work:

it('', async () => {
   const httpError = new HttpError(404, 'Not found.');
   await expect(Promise.reject(new HttpError(404, 'Not found.'))).to.be.rejectedWith(httpError);
});

This is - as @meeber kindly pointed out here - because of the strict === comparison of the throws() assertion in Chai itself, as chai-as-promised just emulates Chai's throws().

I personally think that it'd be more useful for errors to have a deep equality check in place. Something like suggested in the linked issue:
.to.deep.throw() => to.be.deep.rejectedWith()
This deep equality check can't be the same as the normal deep equality check for objects, because of the Error's stack-trace property, which of course will be not the same in the most cases.
I guess this approach would also not break the existing usage, so it might be a bit simpler than changing the error comparison as a whole.

@keithamus
Copy link
Member

Hey @janis91 thanks for the issue.

I'm inclined to agree. But actually right now the deep-eql algorithm doesn't do deep equality on errors, as you mention the .stack property is never going to be equal. Are two errors truly deep-eql if they have different stacks? This is a point of contention which has led us this far.

I would say it might be an easier point to make that .[error|throws|rejectedWith](someErrorInstance) could be special cased and documented that it'll only compare the message and constructor properties. However another problem we have is that often times people tack on other properties onto error objects, e.g. .code.

I have no firm opinion any way on this, just presenting the current state of mind here.

@janis91
Copy link
Author

janis91 commented Oct 19, 2017

Hi @keithamus thanks for sharing your thoughts.

I basically like your suggestion. But I would go even further: Beside that we have to distinguish if it is an instanceof Error first, as it is possible to throw any expression in JS, I would suggest to compare every property beside the .stack property. Because as you already pointed out right, it is also possible and in my opinion, even very important that someone can extend the Error class and put some more information into it. This information shouldn't be lost when comparing them.

I hope this goes in a right way.

@keithamus
Copy link
Member

While it is possible to throw any expression we should really only encourage the throwing of Error objects, so I'm not so interested in supporting generic objects.

Let's get more eyes on this problem before moving toward a particular solution. I guess @meeber might have some feedback, given chaijs/chai-as-promised#226 (comment)

@meeber
Copy link
Contributor

meeber commented Oct 20, 2017

I agree with @janis91; I think our deep equality algorithm should special-case Error objects by ignoring the .stack property, but consider all other own and inherited enumerable properties.

Although the semantics are a bit weird, I also think Chai should support .deep.throw(errInstance) in order to perform a deep equality comparison against the thrown error, instead of strict equality. Such a change could ride in the wake of #1021.

@janis91
Copy link
Author

janis91 commented Feb 9, 2018

@meeber @keithamus Is there any progress on this, so far? I could try to help if you let me know where I should start. I think a .deep.throw is a really nice suggestion. Is the PR #1021 still alive?

@meeber
Copy link
Contributor

meeber commented Feb 10, 2018

@janis91 Things are slow-going at the moment, but this is still on the radar, including a semi-related issue in #907. PR #1021 is still alive, waiting on a follow-up review of chaijs/check-error#18.

@keithamus
Copy link
Member

We've got a lot of PRs and issues open around this. It's definitely something on the radar, and we're trying to close a lot of issues so we can stay focussed. Let's close this so we can keep focussed.

@MicahZoltu
Copy link

MicahZoltu commented Jun 15, 2018

I just upgraded Chai and a test that was previously passing is now failing because the following is true:

expect(new Error('apple')).to.not.deep.equal(new Error('apple'));

Reading through issue backlog, it appears that Error comparisons used to only do a type check during deep compare, but were changed to do full comparisons. Unfortunately this puts me in an awkward situation because in my real scenario the error is a property on a nested object, so I don't see any way to compare other than to manually compare every property of the entire object tree except for the error.

Are there any workarounds for those of us that were broken by the behavior change? Is a rollback the most reasonable solution?

@keithamus
Copy link
Member

@MicahZoltu we'll soon have an .to.be.an.error() matcher which will allow you to do comparisons like .to.be.an.error(Error, /apple/). We're going to put a lot of focus into releasing Chai 5 which will include matchers like this.

@MicahZoltu
Copy link

@keithamus Will that be able to be used as part of a .deep.equal(...)? My real problem looks like:

const error1 = [{a: 'hello', b: new Error('goodbye')}, ...]
const error2 = [{a: 'hello', b: new Error('goodbye')}, ...]
expect(error1).to.deep.equal(error2)

@keithamus
Copy link
Member

@MicahZoltu part of the plan for Chai 5 is to have matchers, so you could do something like:

expect(error1).to.deep.equal({
  a: 'hello',
  b: expect.to.be.an.error(Error, /goodbye/)
})

The exact syntax and mechanics are not yet hashed out, but it would likely be close to something like this.

@janis91
Copy link
Author

janis91 commented Jun 15, 2018

@keithamus That'd be really nice. I think this is quite a big step forward. I like this approach.

@tusharmath
Copy link

tusharmath commented Nov 18, 2019

Actually the assert implementation by node.js actually works —

import * as assert from 'assert'

assert.deepStrictEqual(
  new Error('A'), 
  new Error('A')
) // passes

undergroundwires added a commit to undergroundwires/privacy.sexy that referenced this issue Mar 11, 2022
- Use better error messages with more context.
- Unify their validation logic and share tests.
- Validate also type of the name.
- Refactor node (Script/Category) parser tests for easier future
  changes and cleaner test code (using `TestBuilder` to do dirty work in
  unified way).
- Add more tests. Custom `Error` properties are compared manually due to
  `chai` not supporting deep equality checks (chaijs/chai#1065,
  chaijs/chai#1405).
@h-unterp
Copy link

h-unterp commented Jul 6, 2022

@keithamus

@MicahZoltu part of the plan for Chai 5 is to have matchers, so you could do something like:

expect(error1).to.deep.equal({
  a: 'hello',
  b: expect.to.be.an.error(Error, /goodbye/)
})

The exact syntax and mechanics are not yet hashed out, but it would likely be close to something like this.

Wondering what is happening with Chai 5?

@keithamus
Copy link
Member

Not a lot. Lack of time and developer burnout mean there has been little progress on Chai. Recruiting for maintainers is challenging, and retaining maintainers is even harder. It's a struggle to make changes to 4.x that maintain backwards compatibility while also moving the library forward, as an example take a look at the recent issues around ES Modules.

@h-unterp
Copy link

h-unterp commented Jul 11, 2022

Understood and thank you for sharing this with me. I don't have a lot of time, but I'm also not saying no, just working on a startup which is taking about 200% of my time ;-) One incentive to help attract maintainers is that github copilot comes free for maintainers of large projects: https://copilot.github.com I've been using it 20 days....and I'm def going to continue with it.

LarrMarburger pushed a commit to LarrMarburger/privacy.sexy that referenced this issue Nov 16, 2023
- Use better error messages with more context.
- Unify their validation logic and share tests.
- Validate also type of the name.
- Refactor node (Script/Category) parser tests for easier future
  changes and cleaner test code (using `TestBuilder` to do dirty work in
  unified way).
- Add more tests. Custom `Error` properties are compared manually due to
  `chai` not supporting deep equality checks (chaijs/chai#1065,
  chaijs/chai#1405).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants