chai review
Chai 6.2.2 is a test-runner-neutral assertion library for Node and browsers. It supplies expect, assert, and should interfaces over the same assertion engine, including deep equality, property paths, collection membership, numeric ranges, and synchronous exception checks. Chai does not discover tests, create spies, fake timers, or await rejected promises by itself. Those jobs belong to a runner or plugin. The current patch removes a BigInt literal from closeTo so older parsers can load that assertion implementation. Our browser build measured 63.9 KB minified and 16.6 KB gzipped, which is sizeable for assertions that should usually stay outside production code.
Chai 6.2.2 installed in 0.4 seconds as one 1 MB package with no audit findings in our sandbox, but its full browser import reached 16.6 KB gzipped. Use it when a runner-neutral assertion language or a Chai plugin is a real requirement; otherwise node:assert/strict or the matchers already built into your test runner are simpler.
We installed it
| Install | ✓ · 0.4s | 1 package on disk · 1 MB |
| Import | ✓ | ESM import works · require() works · ESM package |
| Browser | 16.6 KB | gzipped (63.9 KB minified), bundled with esbuild |
| Types | — | no TypeScript types found |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does chai install cleanly?
Yes. In a fresh container with an empty cache, npm install chai finished in 0.4s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.
How much does chai add to a browser bundle?
16.6 KB gzipped (63.9 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does chai work with both ESM and CommonJS?
Yes. Both import 'chai' and require('chai') worked in Node 22 in our run. The package is published as ESM.
Does chai include TypeScript types?
No type declarations were found in our install, so TypeScript users need their own declarations.
chai or should: which should you use?
should: Choose it for should-style assertions when modifying Object.prototype is an accepted project convention. Chai 6.2.2 installed in 0.4 seconds as one 1 MB package with no audit findings in our sandbox, but its full browser import reached 16.6 KB gzipped.
When should you not use chai?
Node's built-in node:assert/strict already covers the suite; Chai would add a 168 KB package and another assertion vocabulary.
Use it if
- A Mocha or custom test setup needs readable assertions without changing its runner or lifecycle hooks.
- The suite mixes fluent expect calls with function-style assert calls and should share one failure model.
- Tests need deep property, subset, member, exception, or tolerance assertions beyond Node's basic examples.
- An established Chai plugin supplies a specific matcher that the team already relies on.
- Node's built-in node:assert/strict already covers the suite; Chai would add a 168 KB package and another assertion vocabulary.
- The project uses Vitest or Jest matchers throughout. Mixing Chai chains into those suites makes extensions and failure output less consistent.
- You need spies, stubs, mocks, or fake timers. Chai core has none, so use Sinon or the test runner's mocking API.
- Promise rejection assertions must work without plugins. Chai core checks synchronous throws, while chai-as-promised is a separate compatibility decision.
- Production browser code would import the assertion library. Our full import cost 63.9 KB minified and 16.6 KB gzipped, so runtime validation needs a purpose-built validator instead.
Setup reality
We installed Chai 6.2.2 in a fresh Node 22 Bookworm sandbox. npm finished in 0.4 seconds and left one package using 1 MB on disk. Chai itself was 168 KB unpacked, declared no direct or peer dependencies, and npm audit reported 0 known vulnerabilities. Its engine floor is Node 18. The package is ESM without an exports map; both require() and ESM import worked in our checks. We found no TypeScript declaration files.
Install it as a development dependency and import one interface explicitly. expect and assert do not modify globals. Calling should() adds a should getter to Object.prototype, which can surprise code that enumerates inherited properties. The register entry points can create globals for a runner preload, but local imports make test ownership easier to see. No credentials or configuration file are required.
Chai only evaluates the value presented to an assertion. For a synchronous exception, pass a function instead of calling it first. For a promise, await the promise and assert its result with core Chai, or add a promise plugin and await that plugin's assertion. Plugin registration mutates the imported Chai instance, so run setup before test modules and check that each plugin supports Chai 6.
The browser build from our full namespace import was 63.9 KB minified and 16.6 KB gzipped. Tree-shaking may differ with narrower imports, but test code should be excluded from production bundles. Version 6.2.2 changes closeTo internals to avoid a BigInt literal parsing problem; its public tolerance remains an absolute delta. Chained words such as to and be are language helpers, while deep, own, nested, ordered, any, and all change assertion behavior.
Patterns
Check primitive results check-scalars
import { expect } from 'chai';
expect(status).to.equal('ready');
expect(count).to.be.greaterThan(0);
expect(cached).to.equal(true);equal uses strict equality. The words to and be only make the chain read naturally.
Compare nested values compare-deep-values
expect(actual).to.deep.equal({
id: 7,
labels: ['paid', 'priority'],
owner: { active: true },
});deep changes object and array comparison from reference identity to recursive value comparison.
Use the assert interface use-assert-functions
import { assert } from 'chai';
assert.strictEqual(response.status, 200);
assert.deepEqual(response.body, { ok: true });
assert.match(response.type, /^application\/json/);assert exposes function calls from the same library, which suits suites that avoid fluent chains.
Check a synchronous error assert-sync-throw
expect(() => decodeToken('bad')).to.throw(TypeError, /token/);
expect(() => decodeToken(validToken)).not.to.throw();Give expect a function. If decodeToken runs before expect, Chai cannot inspect the thrown error.
Read a property path inspect-nested-property
expect(payload).to.have.nested.property('users[0].role', 'admin');
expect(payload).to.have.own.property('requestId');nested interprets dots and brackets as a path. own restricts the check to properties on the object itself.
Compare members without order match-unordered-members
expect(['write', 'read']).to.have.members(['read', 'write']);
expect(rows).to.have.deep.members([{ id: 1 }, { id: 2 }]);members ignores order. Add ordered when position is part of the contract.
Match selected fields check-object-subset
expect(order).to.deep.include({
state: 'paid',
currency: 'USD',
});
expect(order).to.include.all.keys('id', 'createdAt');A subset check leaves unrelated response fields free to change without weakening the fields named here.
Allow floating-point error compare-with-tolerance
expect(0.1 + 0.2).to.be.closeTo(0.3, 1e-12);
expect(progress).to.be.within(0, 1);closeTo uses an absolute delta. Chai 6.2.2 changed its internal BigInt handling without changing this API.
Check strings and arrays check-containment
expect(message).to.include('saved');
expect(scopes).to.include('reports:read');
expect(scopes).to.include.members(['profile:read']);include on an array checks membership. include.members permits values beyond the expected subset.
Register one plugin register-plugin
import * as chai from 'chai';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);
chai.expect(save).to.have.been.calledOnceWith(42);use changes this Chai instance. Load setup before test files and verify that the plugin supports Chai 6.
Assert an awaited value test-promise-result
const user = await loadUser(42);
expect(user).to.deep.include({ id: 42, active: true });Core Chai does not await application work. Resolve the promise before making a normal assertion.
Limit assertion output configure-diff-output
import { config, expect } from 'chai';
config.truncateThreshold = 120;
expect(actualPayload).to.deep.equal(expectedPayload);truncateThreshold controls displayed value length in failures and applies to the imported Chai instance.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| should | npm | Choose it for should-style assertions when modifying Object.prototype is an accepted project convention. |
| expect | npm | Choose it for a smaller standalone expect API outside Jest or Vitest. |
| sinon | npm | Choose it when spies, stubs, mocks, and fake timers are the actual missing test tools. |
More testing guides
pytest · vitest · jsdom · playwright · coverage · axe-core · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

