A matcher is the assertion that connects a real value to an expectation. When a test fails, it is the matcher that phrases the error message: it decides whether the engineer sees the exact cause or a vague 'something is off'. So the choice of matcher is not cosmetics - it is part of the diagnosis. Take an order-checkout service: the payOrder function returns a status and a payment DTO, and how we check that decides whether we catch a regression or let it slip.
A beginner's natural reaction is to grab the most general matcher and not think. toBeTruthy checks that a value is not empty; toBeDefined checks that it is not undefined. They read like 'just make sure it is there', and the test turns green at once. The temptation is understandable: the goal feels like 'get to green', and a broad matcher gets there fastest and on almost any value.
It breaks exactly where you needed protection. expect(status).toBeTruthy() passes for 'paid', for 'refunded', and for 'pending' - for any non-empty string. The test claims the status exists, not that it is correct. If the payment returns 'refunded' instead of 'paid', the suite stays green and the bug ships. The broad matcher verified the presence of a value, not the system's promise.
Precision starts with a deliberate choice. toBe compares with Object.is - it is about primitives and reference identity: 'paid' equals 'paid', the same object. toEqual compares structurally, recursively over fields, and ignores fields whose value is undefined. toStrictEqual is structural too but stricter: it distinguishes undefined fields, array sparseness, and object type. The rule is simple - pick the matcher for what you promise: an exact value, a shape, or a shape plus a type.
When a DTO is large but only part of it matters, toMatchObject checks a subset of fields without describing the whole object. toContainEqual finds an array element structurally equal to a sample - handy for cart line items. For floating-point numbers, direct equality is deceptive: 0.1 + 0.2 is not 0.3 because of binary representation, so sums and shares are checked with toBeCloseTo at the precision you need.
Errors are a contract too. toThrow(DomainError) checks the exception class, and toThrow('Total cannot be negative') checks that the message contains the needed substring; together they pin down that the code fails predictably rather than just 'failing somehow'. For dependency calls, toHaveBeenCalledWith paired with expect.objectContaining asserts that save received an object with the right id, without binding to the rest of the argument's fields.
When the same check with its own diagnostics repeats across many tests, it is worth extracting into a domain matcher via expect.extend. The matcher receives the real value and the expectation, and returns pass plus a message function that builds a clear error string. The gain is not a shorter call but the diagnostics: a failure says outright 'expected order 42 to have status paid, got refunded'. The cost is code you have to maintain, so a custom matcher is justified only when it truly improves error readability at scale.
A telling failure looks like this: a suite of hundreds of toBeTruthy calls burns green for months and catches not a single status swap, because each such test only checks 'the value is non-empty'. A precise matcher flips the picture: when it fails, the message itself names the broken promise - which field, which value was expected, and what arrived. Precise matchers produce precise errors, and that is what turns a failing test from a riddle into a ready diagnosis.
expect(status).toBe('paid')
expect(dto).toEqual({ id: '42', role: 'editor' })
expect(dto).toMatchObject({ role: 'editor' })
expect(0.1 + 0.2).toBeCloseTo(0.3)
expect(items).toContainEqual({ sku: 'A', qty: 2 })
expect(fn).toThrow(DomainError)
expect(fn).toThrow('Total cannot be negative')
expect(save).toHaveBeenCalledWith(expect.objectContaining({ id: '42' }))expect.extend({
toHaveStatus(order, expected) {
const pass = order.status === expected
return {
pass,
message: () =>
`expected order ${order.id} to have status ${expected}, got ${order.status}`,
}
},
})
expect(order).toHaveStatus('paid')