skip to content

In Playwright, what does expect.soft() do that a plain expect() does not?

level: juniorimportance: must knowfreq 56%

answer

  1. One failing check, test keeps going
  2. Failure is recorded, never thrown
  3. Run ends red either way
  4. Same matchers, different failure handling
  5. Costs one timeout per failing check

basics

~10 s

A soft assertion records its failure and lets the test keep running, while a plain expect throws and stops the test at that line. Both end with the test reported as failed.

solid answer

~40 s

`expect.soft(value, message?)` offers the same matchers as `expect()`, but a failure is collected instead of thrown, so execution continues to the next statement. One run can then report several problems on a bank statement page - a wrong balance, a disabled export button, a short transaction table - rather than only the first. When the test ends, the runner reports every collected failure and marks the test failed; soft never means optional. Two catches matter. Soft only stops the *assertion* from throwing: if the element the next line clicks is genuinely missing, that line still fails, usually with a worse message. And each failing soft check spends its full expect timeout (5 seconds by default) before giving up, so a screen of soft failures makes a red run noticeably slower than a green one.

code

typescript · 13 lines
typescript
import { test, expect } from '@playwright/test';

test('bank statement summary', async ({ page }) => {
  await page.goto('/accounts/1/statement');

  // Hard: everything below is meaningless if the statement never rendered.
  await expect(page.getByTestId('statement')).toBeVisible();

  // Soft: three independent observations, all reported in one run.
  await expect.soft(page.getByTestId('balance')).toHaveText('$1,204.55');
  await expect.soft(page.getByRole('row')).toHaveCount(13);
  await expect.soft(page.getByRole('button', { name: 'Export' })).toBeEnabled();
});

go deeper

for a junior

Recall the one-line difference: soft records the failure and carries on, plain expect throws and stops the test there. Both finish with the test failed.

for a middle

Explain the mechanics - the error is collected on the running test instead of thrown, the run reports all of them, and the matcher's waiting behaviour is unchanged.

for a senior

Show you know the costs: one full expect timeout per failing soft check, and a cascade of derived errors when the checks were not actually independent of each other.

for a principal

Own the trade. Soft buys one-run diagnosis and pays in wall-clock time and noise on failure; state where in a test that exchange is worth making and where an anchor must stay hard.

## Two ways an assertion can fail In the Playwright test runner, `expect()` is a **throwing** assertion. When its matcher gives up it raises an error, the error unwinds out of your test function, the runner catches it, and every statement after that line is skipped. `expect.soft()` presents the same matcher surface with different failure handling: the error is **recorded against the running test and not thrown**, so control simply moves on to the next statement. Both forms accept the same optional message as a second argument - `expect.soft(locator, 'balance widget').toHaveText('$1,204.55')` - and both work with the retrying, web-first matchers as well as with plain value matchers. Nothing about the matcher itself changes; only what happens at the moment it fails. ## What "soft" does not mean - **Not optional.** A single failing soft assertion fails the test, exactly like a hard one. - **Not a warning.** There is no separate "soft failure" outcome; the run ends red. - **Not a retry or flakiness setting.** Retries, timeouts and the matcher's own waiting are untouched. - **Not protection for the lines below it.** Soft stops the *assertion* from throwing, nothing else. If the export button really is absent, the `click()` two lines later still fails - and with a less specific message than the assertion would have produced. ## The trade, side by side | | `expect(value)` | `expect.soft(value)` | |---|---|---| | On failure | throws at once | records the error, returns | | Rest of the test body | skipped | keeps running | | Failures surfaced per run | the first one | all of them | | Final test status | failed | failed | | Time spent failing | one timeout | one timeout per failing check | ## Where the recorded failures go A soft failure is appended to the running test's error list, reachable mid-test as `test.info().errors`. That is what makes the usual guard possible: after a block of soft checks, assert `expect(test.info().errors).toHaveLength(0)` and the test stops hard before an expensive follow-up phase. When the test finishes, the runner surfaces the whole list, so the report carries every soft failure with its own matcher output rather than only the first one. ## The price of collecting failures 1. **Timeout multiplication.** A failing retrying matcher spends the full expect timeout - 5 seconds unless you have changed it - before it gives up. Eight soft checks against a page that never rendered cost eight timeouts instead of one, so a broken run takes far longer than a healthy one. 2. **Cascade noise.** When the checks are not independent, one broken thing produces a page of derived failures and the root cause is buried among them. 3. **Weaker triage.** Ten errors on one test read as ten problems until someone reads them carefully; a hard failure at the anchor would have said "the page never loaded" in one line. ## Using it well on a real page Take a bank statement page with a balance widget, a transaction table and an export button. The pattern that pays is a hard **anchor** followed by a block of **independent observations**: - Assert hard that the statement container rendered - nothing below it means anything otherwise. - Then check softly the formatted balance, the row count, the first row's description and the export button's state. These are independent facts about one rendered page, so collecting all four turns three debugging cycles into one. - Pass a message argument on any soft check whose matcher output would not identify it on its own. - Gate the next phase - clicking Export, waiting on a download - on `test.info().errors` when continuing over a known-bad page would only manufacture noise. ## The mental model A hard assertion is a **stop condition**; a soft assertion is an **entry in a defect list**. The list is still a failing inspection - you simply finish walking the room before you file it. Reach for soft when the extra walking is genuinely informative, and keep the stop condition wherever walking on would tell you nothing new.

  • If one soft assertion fails and a later line throws for a different reason, what does the report show?
    Both. The soft failure was already recorded, and the thrown error is added when it aborts the test. The report lists every error the test accumulated, so you see the recorded soft failure alongside the hard one that ended the run.
  • Does making an assertion soft change how long it waits before failing?
    No. Soft changes only what happens after the matcher gives up. A retrying matcher still polls for the full expect timeout, 5 seconds by default, before recording a failure - which is exactly why a block of failing soft checks makes a run slower.
  • Can a soft assertion failure ever leave the test passing?
    No. A recorded soft failure fails the test when it finishes, and nothing in the test body can remove it. If you want a check whose failure does not fail the test, do not assert it - log it or drop it.

A hard assertion is a circuit breaker that cuts the power at the first fault; a soft assertion is a snag list - you keep walking the flat and write down every defect, and the flat still fails inspection.

saying these in an interview costs you the question

  • Thinking a failing soft assertion leaves the test passing
  • Calling soft assertions warnings you are free to ignore
  • Assuming code after a failing soft check is skipped
  • Believing soft mode turns off the matcher's retry timeout
  • Making every assertion soft so nothing ever stops a test