# Exceptions

We all write tests that deal with exceptions and promise rejections. For example:
- Assert that a call to an invalid API endpoint rejects the promise with an HTTP 404 error.
- Assert that rendering a component without a required property throws an exception.

Usually, you might wrap these function calls inside a `try/catch` statement and write assertions for the error object.

```ts
import { test } from '@pawel-up/lupa/testing'

// ❌ The Wrong Way
test('validate property', ({ assert }) => {
  try {
    validateProperty(undefined)
  } catch (error) {
    assert.equal(error.message, 'Property is required')
  }
})
```

There are two main problems with the `try/catch` statement in testing:

- You will get a "false positive" passing test if the code inside the `try` block **never** raises an exception!
- The `try/catch` statement adds visual noise to your tests, especially when writing nested statements.

To solve this, Lupa's bundled assertion library has dedicated methods to assert a function call throws an exception safely.

## Using dedicated assertion methods

### `assert.throws`

The `assert.throws` method accepts a function as the first parameter and the error message (or regex) you expect as the second parameter.

This method only works with synchronous function calls.

```ts
import { test } from '@pawel-up/lupa/testing'

test('validate property', ({ assert }) => {
  assert.throws(
    () => validateProperty(undefined),
    'Property is required'
  )
})
```

### `assert.rejects`

Similar to `assert.throws`, the `assert.rejects` method also accepts a function and the error message for assertion. However, in this case, the callback function **must** return a Promise.

```ts
import { test } from '@pawel-up/lupa/testing'

test('do not fetch invalid user', async ({ assert }) => {
  
  await assert.rejects(
    async () => fetchUser(9999),
    /User not found/
  )
  
})
```

## Asserting Custom Properties

If your application throws custom error classes with additional properties, you can verify them by awaiting `assert.rejects()` or assigning `assert.throws()` to a variable.

Both methods **return the caught error**, allowing you to run standard assertions against its properties. You can also pass a generic type to strongly type the returned error.

```ts
import { test } from '@pawel-up/lupa/testing'

test('assert custom error properties', async ({ assert }) => {
  // Strongly type the returned error by passing the generic
  const err = await assert.rejects<ValidationException>(
    async () => validateConfig({}),
    ValidationException
  )

  // Now you can assert on custom properties effortlessly!
  assert.equal(err.code, 'E_MISSING_PROP')
  assert.deepEqual(err.missingKeys, ['timeout', 'retries'])
})
```

## High order assertion

An elegant alternative API that is completely independent of the assertion library is to expect a test to throw an exception directly on the test instance using the `test.throws` method.

Let's re-write the above two examples with a high-order assertion:

```ts
import { test } from '@pawel-up/lupa/testing'

test('validate property', () => {
  // If this doesn't throw, the test fails
  validateProperty(undefined)
})
.throws('Property is required')
```

```ts
import { test } from '@pawel-up/lupa/testing'

test('do not fetch invalid user', async () => {
  
  /**
   * This call will throw an exception, and
   * there is no need to handle it within the test callback
   */
  await fetchUser(9999)
  
})
.throws(/User not found/)
```
