Chai.js Cheatsheet - expect, should & assert Assertions
This reference is for JavaScript developers writing tests with Mocha, Jest integrations, or plain Node. Chai gives three interchangeable styles (expect, should, assert); this cheatsheet shows the same check written each way. It covers equality, deep comparison of objects/arrays, type and inclusion checks, thrown-error assertions, numeric bounds, and plugin-backed helpers such as eventually (chai-as-promised) and HTTP status (chai-http). After reading you should be able to write readable assertions for values, collections, thrown errors, and promises, and know when deep.equal beats equal.
Typical Use Case
For front-end and Node.js test authors who describe business rules in BDD style. All three assertion styles appear here so you can read whichever one a given repository uses: expect is the most universal and reads closest to natural language, should is the shortest to write but has sharp edges around null values, and assert is the TDD flavour with more explicit function calls. The sheet covers how to choose between equality and deep equality, where .equal is strict while .deep.equal compares layer by layer, plus type and containment assertions, the four ways of asserting on exceptions (by type, by message pattern, and asserting that nothing is thrown), and the Promise and HTTP assertions that only work once a plugin is registered. The troubleshooting entries explain why a question mark renders as ??, the difference between .own.property and .property, and why should crashes on null.
Expect Style (BDD) 8
expect(value).to.equal(expected)expect(value).to.be.a("string")expect(array).to.include(item)expect(value).to.be.trueexpect(value).to.not.be.nullexpect(array).to.have.lengthOf(3)expect(value).to.existexpect(obj).to.have.property("key", "value")Should Style (BDD) 5
value.should.equal(expected)value.should.be.a("string")array.should.include(item)value.should.be.truevalue.should.existAssert Style (TDD) 7
assert.equal(actual, expected)assert.notEqual(actual, expected)assert.deepEqual(actual, expected)assert.isTrue(value)assert.isArray(value)assert.throws(fn)assert.fail(actual, expected, "message")Equality & Deep Comparison 6
expect(value).to.deep.equal(obj)expect(value).to.eql(obj)expect(value).to.not.equal(other)expect(obj).to.have.own.property("key")expect(obj).to.have.nested.property("a.b.c")expect(arr).to.deep.equal([1, 2, 3])Type & Inclusion 6
expect(value).to.be.an("array")expect(value).to.be.an.instanceof(Date)expect(str).to.match(/^foo/)expect(str).to.have.string("bar")expect(obj).to.include({ key: "value" })expect(arr).to.have.members([1, 2, 3])Exceptions & Throws 6
expect(fn).to.throw(Error)expect(fn).to.throw("error message")expect(fn).to.throw(/message/)expect(fn).to.not.throw()assert.throws(fn, TypeError)assert.doesNotThrow(fn)Numbers, Booleans & Plugins 6
expect(num).to.be.above(5)expect(num).to.be.at.least(5)expect(num).to.be.below(10)expect(num).to.be.within(1, 10)expect(promise).to.eventually.equal("resolved")expect(res).to.have.status(200)Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
expect(value).to.equal(x) | Strict equality assertion (===) | expect(value).to.equal(expected) |
expect(value).to.be.a("string") | Type assertion | expect(value).to.be.a("string") |
expect(arr).to.include(item) | Assertion that a collection includes an element | expect(array).to.include(item) |
expect(value).to.be.true | Boolean truth assertion | expect(value).to.be.true |
expect(value).to.not.be.null | Non-null assertion | expect(value).to.not.be.null |
expect(arr).to.have.lengthOf(3) | Length assertion | expect(array).to.have.lengthOf(3) |
expect(value).to.exist | Existence assertion (neither null nor undefined) | expect(value).to.exist |
expect(obj).to.have.property("k", "v") | Assert that a property exists and its value matches | expect(obj).to.have.property("key", "value") |
value.should.equal(x) | should-style equality assertion | value.should.equal(expected) |
value.should.exist | should-style existence assertion | value.should.exist |
assert.equal(a, b) | Loose equality comparison (==) | assert.equal(actual, expected) |
assert.notEqual(a, b) | Loose inequality assertion | assert.notEqual(actual, expected) |
assert.deepEqual(a, b) | Recursively compare every level of an object/array | assert.deepEqual(actual, expected) |
assert.isTrue(v) / assert.isArray(v) | Boolean and array type assertions | assert.isArray(value) |
assert.throws(fn) | Assert that calling it throws | assert.throws(fn) |
assert.fail(a, b, "message") | Fail the test unconditionally, for unreachable branches | assert.fail(actual, expected, "message") |
.to.deep.equal(obj) | Deep-compare objects/arrays | expect(value).to.deep.equal(obj) |
.to.eql(obj) | Alias of .deep.equal | expect(value).to.eql(obj) |
.to.not.equal(other) | Inequality assertion | expect(value).to.not.equal(other) |
.to.have.own.property("k") | Own-property assertion, excluding the prototype chain | expect(obj).to.have.own.property("key") |
.to.have.nested.property("a.b.c") | Nested-path property assertion | expect(obj).to.have.nested.property("a.b.c") |
.to.be.an("array") | Array type assertion | expect(value).to.be.an("array") |
.to.be.an.instanceof(Date) | Instance type assertion | expect(value).to.be.an.instanceof(Date) |
.to.match(/^foo/) | Regex match assertion | expect(str).to.match(/^foo/) |
.to.have.string("bar") | String substring assertion | expect(str).to.have.string("bar") |
.to.include({ key: "value" }) | Partial object inclusion assertion | expect(obj).to.include({ key: "value" }) |
.to.have.members([1, 2, 3]) | Assert that array members match exactly | expect(arr).to.have.members([1, 2, 3]) |
.to.throw(Error) | Assert it throws an exception of the given type | expect(fn).to.throw(Error) |
.to.throw("error message") | Assert the error message contains the substring | expect(fn).to.throw("error message") |
.to.throw(/message/) | Match the error message with a regex | expect(fn).to.throw(/message/) |
.to.not.throw() | Assert it does not throw | expect(fn).to.not.throw() |
assert.throws(fn, TypeError) | TDD-style throw assertion | assert.throws(fn, TypeError) |
assert.doesNotThrow(fn) | TDD-style no-throw assertion | assert.doesNotThrow(fn) |
.to.be.above(5) / .to.be.below(10) | Greater-than / less-than assertion | expect(num).to.be.above(5) |
.to.be.at.least(5) | Greater-than-or-equal assertion | expect(num).to.be.at.least(5) |
.to.be.within(1, 10) | Range assertion, bounds inclusive | expect(num).to.be.within(1, 10) |
.to.eventually.equal("resolved") | Promise assertion, requires the chai-as-promised plugin | expect(promise).to.eventually.equal("resolved") |
.to.have.status(200) | HTTP status assertion, requires the chai-http plugin | expect(res).to.have.status(200) |
Common pitfalls
SymptomUsing should-style assertions on a possibly-null value crashes the test while reading .should, aborting the suite.
CauseThe should style requires reading a property off the object, so null/undefined throws before the assertion even runs.
FixSwitch to the expect style: expect(value).to.not.be.null, where the assertion function safely accepts empty values first.
SymptomThe fields of the object clearly match, yet .to.equal fails.
Cause.equal uses strict equality and compares addresses for reference types; two objects with identical content but different references will not pass.
FixUse .to.deep.equal or .eql to compare content layer by layer.
SymptomAn array assertion fails even though it "looks like the same array."
CauseStrict equality compares references; the array rebuilt in the test is not the same reference as the one in the object under test.
FixCompare content with expect(arr).to.deep.equal([1, 2, 3]), or compare only the member set with .have.members.
SymptomYou want to assert "this property exists on the object," but inherited prototype properties also pass.
Cause.property walks the prototype chain, whereas checking an own property should use .own.property.
FixUse expect(obj).to.have.own.property("key") to consider only the own properties of the object.
SymptomThe error message shows expected as ??, so you cannot tell what is being compared.
CauseThe expression passed as the expected value itself evaluated to undefined, so Chai can only render undefined as a placeholder.
FixCompute the expected value before passing it in, or add a custom message to the assertion to ensure a definite value is compared.
SymptomA .to.throw() assertion passes even though the function actually threw a different error.
CauseWith no arguments it only checks whether something was thrown, not the type or the message.
FixBe specific: .to.throw(Error) checks the type, .to.throw("message") or .to.throw(/message/) checks the message content.
SymptomAsserting the resolved value of a Promise reports eventually is not a function.
CausePromise-related assertions depend on the chai-as-promised plugin, which is not registered.
FixRegister chai-as-promised at the test entry point before using chained forms like expect(promise).to.eventually.
Troubleshooting
1Confirm the actual type of the value before choosing an assertion
expect(value).to.be.a("string")When a type assertion fails it prints the actual type, faster than guessing the assertion syntax.
2Distinguish shallow from deep comparison
expect(value).to.deep.equal(obj)If the content matches but .to.equal fails, that signals you should switch to a deep comparison.
3Compare array members without caring about order
expect(arr).to.have.members([1, 2, 3])It passes when the member sets match, even if the order differs.
4Verify the error branch actually throws the given exception
expect(fn).to.throw(TypeError)Wrapping the function under test in the assertion fails immediately with the actual error if nothing is thrown or the type differs.
5Confirm a property is an own property, not inherited from the prototype
expect(obj).to.have.own.property("key")If .property passes but .own.property fails, the property comes from the prototype chain.
6Assert the HTTP response status
expect(res).to.have.status(200)Register the chai-http plugin first; on failure it prints the actual status code.
Command Examples
One BDD chain covering type, content, and null
const res = await parse(body);
expect(res).to.be.an("object");
expect(res.data).to.have.lengthOf(3);
expect(res.error).to.not.exist;
expect(res.data[0].name).to.equal("tom");Chai BDD assertions are chainable: expect appears once, and .to / .be / .have are interchangeable, so the failure message reads like a path to the offending property. One chain beats four separate expects when you want to see where it broke.
Deep comparison of objects and arrays
expect(actual).to.deep.equal({ id: 1, tags: ["a", "b"] });
// eql is an alias of deep.equal, in a shorter style
expect(rows).to.eql([{ id: 1 }, { id: 2 }]);
// Array members must match exactly, order-independent
expect(ids).to.have.members([3, 1, 2]);equal is strict ===, so two structurally identical objects with different references always fail — use deep.equal / eql for structure. members demands an exact set (any order), which is what you want when asserting "exactly these rows, nothing extra".
Covering the error branch: three ways to match a throw
expect(() => JSON.parse("{")).to.throw();
expect(() => parse("x")).to.throw(TypeError);
expect(() => parse("x")).to.throw(/invalid/);
// Also assert the branch that should not throw, or it passes silently
expect(() => parse("{}")).to.not.throw();A bare to.throw() only proves that something threw, so a bug with the wrong error type or message still passes. Narrow it to a type plus a message regex, and add to.not.throw() on the happy path to cover both directions.
Asserting only the fields you care about
expect(user).to.include({ role: "admin" });
expect(user).to.have.own.property("id");
expect(user).to.have.nested.property("profile.address.city");include is a partial match, so newly added fields do not break the test. own.property looks at own properties only — an inherited one still counts as present and misleads you. nested.property walks a dotted path and pinpoints the missing level.
Numeric ranges plus async assertions
expect(score).to.be.within(0, 100);
expect(save()).to.eventually.equal("ok");
// Requires registering chai-as-promised / chai-http first
// expect(res).to.have.status(200);within includes both endpoints, which saves you from hand-writing above(0).and.below(100) and dropping a boundary. eventually and status need plugins such as chai-as-promised and chai-http — without registering them you just get an unknown assertion method.
Common Pitfalls
- expect(value).to.equal(other) is strict ===, so two structurally identical objects with different references always fail. Forgetting deep.equal produces a "they look the same but it fails" phantom bug.
- Once a property in a BDD chain is undefined, everything after .to.have.property explodes into unrelated errors. Bisect from the head of the chain instead of editing several assertions at once.
- assert.equal uses loose ==, so "1" and 1 count as equal. On fields that mix strings and numbers it hides type errors; prefer a strict assertion.
- expect(fn).to.throw() only proves that something threw. A wrong business error type still passes, so error branches must constrain both the type and the message.
- Promise assertions need chai-as-promised registered first, otherwise eventually / rejected do not exist and the failure reads "assertion is not a function" — which looks like a broken test environment rather than a missing plugin.
- With only expect(fn).to.throw() and no positive-path assertion that the call does not throw, a broken error branch (not throwing when it should) is never detected at all.
Tips
- expect and should are BDD style; assert is TDD style.
- should requires calling .should on an object and may error on null/undefined.
- Use .deep.equal for deep comparison; .equal for shallow comparison.
- chai-as-promised adds Promise assertions; chai-http adds HTTP request assertions.
- Chain methods like to, be, have only improve readability and do nothing functionally.
FAQ
When should I use should versus expect in chai?
expect(value).to.equal(expected) passes the subject as the first argument and puts the assertion after it; the should style hangs the assertion off the subject, as in value.should.be.a("string"). Two practical differences: expect reads fine with strict null checking in editors, while should works by adding a property to the object — calling .should on null or undefined throws immediately, so nullable values must be asserted with expect, as in expect(value).to.not.be.null. assert is the TDD flavour, written assert.equal(actual, expected), and looks like classic unit-testing frameworks. Pick one per project and stay consistent.
What is the real difference between equal and deep.equal?
expect(value).to.equal(expected) is strict equality (===) on references or primitives, so expect([1,2]).to.equal([1,2]) fails — those are two distinct array objects. Only expect(value).to.deep.equal(obj), short form .to.eql, walks the structure recursively and matches arrays and nested objects. Related: expect(obj).to.have.own.property("key") checks own properties without walking the prototype chain, and expect(obj).to.have.nested.property("a.b.c") asserts along a path. When an assertion fails on values that look identical, check for a missing deep first — that is by far the most common cause.
How do I assert that a function throws when testing an error branch?
The expect style offers four granularities: expect(fn).to.throw(Error) checks only the exception type, expect(fn).to.throw("error message") requires an exact message, expect(fn).to.throw(/message/) matches part of it with a regex, and expect(fn).to.not.throw() asserts the happy path. The critical detail is passing the function itself, not its result — expect(fn()).to.throw() evaluates fn first, so the exception escapes before the assertion runs. The assert equivalents are assert.throws(fn, TypeError) and assert.doesNotThrow(fn). To mark a branch that should be unreachable, use assert.fail(actual, expected, "message") so the test fails loudly rather than passing silently.
How do I assert on Promises with chai?
Async assertions are not in chai core — they come from plugins. chai-as-promised is the common one, adding the eventually modifier so expect(promise).to.eventually.equal("resolved") awaits settlement before comparing, with the rejected form used to assert the failure reason, e.g. expect(promise).to.be.rejectedWith(Error). HTTP responses are chai-http: expect(res).to.have.status(200) validates both the status code and body. If your project already runs on Jest, its built-in resolves and rejects matchers cover these cases and the plugins are unnecessary.
How do include and members differ when asserting on an array?
expect(arr).to.include(item) only requires that the element be present — extra elements do not affect the result, which suits "the item the user is looking for is in there". expect(arr).to.have.members([1, 2, 3]) demands exact correspondence: same length and same values, suited to snapshot-style whole-collection checks. expect(obj).to.include({ key: "value" }) is the object counterpart, comparing only the named keys rather than the whole object. For strings use expect(str).to.have.string("bar") for substring matching, and for numbers the above / below / at.least / within(1, 10) family. Picking the wrong one yields assertions that are too loose or too strict.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us