# Swift Assertions: assert, precondition, and fatalError

Source: https://devdocs10.com/articles/swift/swift-assertions  
By Murat Bo · Updated 2026-10-17 · DevDocs10 (https://devdocs10.com)

Swift assertions help you catch invalid assumptions early, fail fast when a program reaches an impossible state, and document the rules your code expects. They are a key part of testing and debugging because they make bugs obvious instead of letting incorrect data flow deeper into your app.

> **Quick answer:** Use `assert` for debug-only checks, `precondition` for conditions that must always be true, and `fatalError` when execution should never continue. In release builds, `assert` is usually removed, while `precondition` and `fatalError` can still stop the program.

> **Difficulty:** Beginner
>
> **You'll understand this better if you know:** basic Swift syntax, how functions return values, and the difference between valid and invalid program states.

## 1. What Are Assertions in Swift?

Assertions are runtime checks that verify assumptions about your code. If an assertion fails, Swift stops execution and reports the failure so you can fix the bug immediately.

- `assert` checks conditions during development.
- `precondition` checks requirements that must be true before code continues.
- `fatalError` stops execution unconditionally with a message.
- They are used to catch logic mistakes, invalid input, and impossible states.

These tools are not for normal user-facing validation. If bad input is expected from the outside world, handle it with optional handling, error throwing, or other control flow instead of crashing.

## 2. Why Assertions Matter

Assertions make bugs easier to find because they fail at the point where the problem starts. That is often much better than allowing a bad value to travel through multiple functions and produce confusing behavior later.

They also make code self-documenting. When you write an assertion, you are stating the assumptions your function or type depends on. Other developers can read those assumptions directly in the code.

Use them when:

- You want to verify a programmer error during development.
- You are checking internal invariants, such as array indexes or state transitions.
- You want to stop execution immediately when continuing would be unsafe or meaningless.

Do not use them for expected runtime conditions like network failures, missing files, or user-entered invalid data.

## 3. Basic Syntax

Swift provides three closely related functions. They all take a Boolean condition or a message, but they serve different purposes.

### Simple `assert`

Use `assert` to check a condition that should be true while debugging.

```swift
let age = 21
assert(age >= 0, "Age cannot be negative.")
```

This says that `age` should never be negative. If it is, your program logic is wrong.

### Simple `precondition`

Use `precondition` when the condition must be true before execution continues.

```swift
let count = 5
precondition(count > 0, "Count must be greater than zero.")
```

This check is stronger than `assert` because it is meant to protect important runtime assumptions.

### Simple `fatalError`

Use `fatalError` when code should never reach a certain path.

```swift
func unsupportedMode() -> Never {
    fatalError("This mode is not supported.")
}
```

`fatalError` does not return, so Swift treats the function as terminating immediately.

## 4. Step-by-Step Examples

### Example 1: Checking a value during development

Imagine a helper that calculates a discount. You can assert that the percentage is within a valid range while you are building and testing the feature.

```swift
func discountedPrice(price: Double, discount: Double) -> Double {
    assert(discount >= 0 && discount <= 1, "Discount must be between 0 and 1.")
    return price * (1 - discount)
}
```

If the check fails, you get a clear signal that the calling code is wrong.

### Example 2: Protecting a required precondition

Use `precondition` when the function cannot continue safely unless its input meets a strict requirement.

```swift
func average(values: [Double]) -> Double {
    precondition(!values.isEmpty, "Cannot compute an average of an empty array.")

    let sum = values.reduce(0, +)
    return sum / Double(values.count)
}
```

The precondition makes the function contract explicit and prevents a divide-by-zero problem.

### Example 3: Reaching an impossible branch

`fatalError` is appropriate when every valid path has already been handled and anything else indicates a programming bug.

```swift
enum Theme {
    case light
    case dark
}

func themeName(theme: Theme) -> String {
    switch theme {
    case .light:
        return "Light"
    case .dark:
        return "Dark"
    }
}
```

In this complete switch, no default branch is needed. If a default branch were required for a larger enum, `fatalError` could be used to flag an impossible state.

### Example 4: Comparing debug and release behavior

This example shows why `assert` is often used for development checks that you do not want to keep in optimized release builds.

```swift
import Foundation

func makeUsername(name: String) -> String {
    assert(!name.isEmpty, "Name should not be empty.")
    return name.trimmingCharacters(in: .whitespacesAndNewlines)
}
```

In a debug build, the assertion helps catch bad test data. In a release build, the check may be removed, so you should not rely on it for user input validation.

## 5. Practical Use Cases

- Checking that an internal counter never becomes negative.
- Verifying a parsing function only receives valid intermediate data.
- Enforcing that a type is only created with supported configuration values.
- Marking code paths that should not be reachable during normal execution.
- Documenting assumptions in utility methods and low-level helpers.

These checks are most useful inside your own codebase, where a failed assertion tells you that a programmer made a mistake.

## 6. Common Mistakes

### Mistake 1: Using `assert` for user input validation

It is tempting to use `assert` whenever a value looks wrong, but user input can be invalid in normal operation. A crash is the wrong response for expected input errors.

**Problem:** This code crashes in debug builds if the email is invalid, but it does not actually handle the situation in a user-friendly way.

```swift
func sendWelcomeEmail(email: String) {
    assert(email.contains("@"), "Invalid email address.")
    // Send the email here.
}
```

**Fix:** Validate the input and return early or throw an error when the value is not acceptable.

```swift
func sendWelcomeEmail(email: String) {
    guard email.contains("@") else {
        return
    }
    // Send the email here.
}
```

The corrected version handles invalid input instead of treating it like a programmer bug.

### Mistake 2: Using `precondition` for something that should be optional

`precondition` is strong, so it should only protect invariants that must hold for the function to work. If a missing value is normal, do not crash.

**Problem:** This code aborts when a nickname is missing, even though a fallback name would be fine.

```swift
func displayName(nickname: String?) -> String {
    precondition(nickname != nil, "Nickname is required.")
    return nickname!
}
```

**Fix:** Use a safe fallback when the value is optional and absence is acceptable.

```swift
func displayName(nickname: String?) -> String {
    return nickname ?? "Guest"
}
```

The corrected version avoids a crash because the absence of a nickname is no longer treated as an impossible state.

### Mistake 3: Forgetting that assertions are not a substitute for safe control flow

Assertions help during development, but they do not replace proper branching logic. If a value can be missing in real usage, unwrap it safely first.

**Problem:** This code force-unwraps an optional after an assertion, which can still crash in release builds if the check is removed.

```swift
func firstCharacter(text: String?) -> Character {
    assert(text != nil, "Text must exist.")
    return text!.first!
}
```

**Fix:** Use optional binding and handle the missing case explicitly.

```swift
func firstCharacter(text: String?) -> Character? {
    guard let text = text, let first = text.first else {
        return nil
    }
    return first
}
```

The corrected version is safe in every build because it does not rely on the assertion for correctness.

## 7. Best Practices

### Practice 1: Use `assert` for internal checks, not public validation

Use `assert` when a failure means your own code is wrong. That keeps the check lightweight and focused on development-time debugging.

```swift
func setProgress(value: Double) {
    assert(value >= 0 && value <= 1, "Progress must stay between 0 and 1.")
}
```

This is a good fit because progress values come from your own logic, not from untrusted input.

### Practice 2: Use clear messages that explain the assumption

Good assertion messages reduce debugging time. State what was expected and, when helpful, why it matters.

```swift
precondition(capacity > 0, "Capacity must be positive before allocating the buffer.")
```

A clear message tells you what broke instead of forcing you to inspect the code path manually.

### Practice 3: Prefer safe control flow for recoverable cases

If the program can continue in a sensible way, use `guard`, optional binding, or error handling. Reserve assertions for impossible or programmer-error states.

```swift
func parsePort(text: String) -> Int? {
    guard let port = Int(text), port > 0 else {
        return nil
    }
    return port
}
```

This approach keeps invalid external data from turning into a crash.

## 8. Limitations and Edge Cases

- `assert` is typically disabled in optimized release builds, so never depend on it for essential logic.
- `precondition` is intended for conditions that must hold in both debug and release behavior, although compiler optimization can affect how the check is handled.
- `fatalError` always terminates execution, so only use it when continuing would be incorrect or dangerous.
- Assertions are for programmer mistakes, not recoverable runtime failures.
- Assertion failures crash the process, which may be appropriate in development but unacceptable in user-facing error paths.
- When code is compiled with optimizations, the exact failure message or backtrace may differ from debug builds.

> **Note:** If you are checking assumptions in a library API, document whether callers must satisfy the condition before calling the function. That helps them understand whether they should expect a crash or a recoverable error.

## 9. Practical Mini Project

Here is a small validation helper for a bank transfer workflow. It uses assertions to protect internal state and `precondition` for required invariants.

```swift
struct Transfer {
    let sourceBalance: Double
    let amount: Double

    func execute() -> Double {
        precondition(amount > 0, "Transfer amount must be positive.")
        assert(sourceBalance >= amount, "Source balance should already have been validated.")
        return sourceBalance - amount
    }
}

let transfer = Transfer(sourceBalance: 250, amount: 40)
let remaining = transfer.execute()
print(remaining)
```

Output (tested with Swift 6.4):

```text
210.0
```

This example shows a realistic split: `precondition` protects required input, while `assert` documents an internal assumption that should already have been checked elsewhere.

## 10. Key Points

- `assert` is best for development-time checks.
- `precondition` protects required runtime conditions.
- `fatalError` is for impossible or unsupported code paths.
- Use assertions for programmer mistakes, not expected user errors.
- Write clear messages so failures are easy to diagnose.
- Do not rely on `assert` for essential release behavior.

## 11. Practice Exercise

- Write a function named `clampedPercentage` that accepts a `Double` between 0 and 1.
- Use `assert` to check the input range during development.
- Return the value unchanged if it is valid.
- Return the midpoint value `0.5` only if you decide to add a fallback path, but keep the function simple.

**Expected output:** A function that accepts valid values and reports invalid ones during debugging.

**Hint:** Combine a range check with a clear assertion message. Do not use the assertion as the only way to handle bad data if the caller may supply invalid input.

**Solution:**

```swift
func clampedPercentage(_ value: Double) -> Double {
    assert(value >= 0 && value <= 1, "Percentage must be between 0 and 1.")
    return value
}
```

## 12. Final Summary

Swift assertions are a fast way to catch incorrect assumptions during development. They help you fail early, keep bugs close to their source, and make your code’s expectations easier to understand.

Use `assert` for debug-time checks, `precondition` for conditions that must always be true, and `fatalError` when execution should never continue. The key skill is choosing the right level of strictness: recover from expected problems, and crash only when the code has reached an invalid state.

As you build larger Swift programs, combine assertions with safe optionals, error handling, and careful API design. That gives you code that is easier to debug without becoming fragile in production.
