Swift Throwing Functions (throws): Syntax, Usage, and Errors

Swift throwing functions let you write functions that can fail in a controlled, explicit way. They are a core part of Swift error handling, and you will use them whenever an operation might not succeed, such as validating input, reading data, or converting values safely.

Quick answer: In Swift, you add throws to a function declaration to say that the function may produce an error. When calling it, you must use try and handle the error with do-catch, or use try? or try! when appropriate.

Difficulty: Beginner

Helpful to know first: You will understand this better if you already know basic Swift syntax, how functions are declared and called, and simple types like String, Int, and Bool.

1. What Is Throwing Functions (throws)?

A throwing function is a function marked with throws to indicate that it might fail and return an error instead of a normal result. Swift makes this explicit so you cannot accidentally ignore possible failures.

In practice, throwing functions help you separate successful results from failure cases in a clean, readable way.

2. Why Throwing Functions (throws) Matters

Without throwing functions, you often end up using special return values, deeply nested conditionals, or extra Boolean flags to report failure. That makes code harder to understand and easier to misuse.

Throwing functions matter because they:

You should use a throwing function when an operation can fail for a meaningful reason and the caller should decide how to handle that failure.

A good rule of thumb is this: if the failure is expected and should be handled differently in different situations, throws is often the right tool.

3. Basic Syntax or Core Idea

Declaring a throwing function

To declare a throwing function, place throws before the return type.

enum PasswordError: Error {
    case tooShort
}

func validatePassword(_ password: String) throws {
    if password.count < 8 {
        throw PasswordError.tooShort
    }
}

This function does not return a value, but it can still fail. If the password is too short, it throws a PasswordError.tooShort error.

Calling a throwing function

When you call a throwing function, you must use try. The most common pattern is do-catch.

do {
    try validatePassword("secret")
    print("Password is valid")
} catch {
    print("Password validation failed")
}

If the function succeeds, execution continues normally. If it throws, control moves to the catch block.

throws vs throw vs try

These terms are closely related and are commonly confused:

enum NumberError: Error {
    case negativeValue
}

func squareRootDescription(for number: Int) throws -> String {
    if number < 0 {
        throw NumberError.negativeValue
    }

    return "Input is \(number)"
}

do {
    let message = try squareRootDescription(for: 9)
    print(message)
} catch {
    print("Could not create description")
}

This example shows all three parts working together.

4. Step-by-Step Examples

Example 1: Throwing when input is invalid

This is the most common beginner example. The function checks a value and throws if the value does not meet the rules.

enum AgeError: Error {
    case tooYoung
}

func registerUser(age: Int) throws {
    if age < 18 {
        throw AgeError.tooYoung
    }

    print("Registration allowed")
}

The function either prints a success message or throws an error. It does not need to return a special failure value.

Example 2: Handling specific errors in catch

You can match individual error cases and respond differently to each one.

enum LoginError: Error {
    case emptyUsername
    case emptyPassword
}

func login(username: String, password: String) throws {
    if username.isEmpty {
        throw LoginError.emptyUsername
    }

    if password.isEmpty {
        throw LoginError.emptyPassword
    }

    print("Logged in")
}

do {
    try login(username: "Taylor", password: "")
} catch LoginError.emptyUsername {
    print("Please enter a username")
} catch LoginError.emptyPassword {
    print("Please enter a password")
} catch {
    print("Unknown login error")
}

This makes your error handling more useful than a single generic message.

Example 3: Returning a value from a throwing function

Throwing functions can still return values when they succeed.

enum ParsingError: Error {
    case invalidNumber
}

func parseScore(from text: String) throws -> Int {
    guard let score = Int(text) else {
        throw ParsingError.invalidNumber
    }

    return score
}

do {
    let result = try parseScore(from: "42")
    print(result)
} catch {
    print("Could not parse score")
}

Here the function returns an Int on success, or throws on failure.

Example 4: Using try?

Sometimes you do not care about the exact error and only want a successful value or nil.

let score = try? parseScore(from: "not a number")
print(score as Any)

try? converts the thrown error into an optional result. If parsing fails, score becomes nil.

Example 5: Using rethrows

A function marked rethrows only throws if one of its function parameters throws. This is common in higher-order functions and utility wrappers.

func performTwice(action: () throws -> Void) rethrows {
    try action()
    try action()
}

enum ActionError: Error {
    case failed
}

do {
    try performTwice {
        throw ActionError.failed
    }
} catch {
    print("Action failed")
}

The wrapper function is not inventing new errors. It only forwards errors from the closure it receives.

5. Practical Use Cases

6. Common Mistakes

Mistake 1: Calling a throwing function without try

This is one of the most common Swift error handling mistakes. If a function is marked throws, Swift requires you to acknowledge that possibility at the call site.

Problem: This code calls a throwing function as if it were a normal function, so Swift reports an error such as Call can throw, but it is not marked with 'try' and the error is not handled.

let result = parseScore(from: "42")
print(result)

Fix: Add try and handle the error, or use try? if an optional result is acceptable.

do {
    let result = try parseScore(from: "42")
    print(result)
} catch {
    print("Parsing failed")
}

The corrected version works because the call site now explicitly handles the possibility of failure.

Mistake 2: Using throw in a function that is not marked throws

Swift only allows throw inside functions that can throw. If the function declaration does not include throws, the compiler rejects it.

Problem: This function tries to throw an error without being declared as throwing, which causes a compile-time error.

enum InputError: Error {
    case empty
}

func checkInput(_ text: String) {
    if text.isEmpty {
        throw InputError.empty
    }
}

Fix: Mark the function with throws so Swift knows the function may send an error.

func checkInput(_ text: String) throws {
    if text.isEmpty {
        throw InputError.empty
    }
}

The corrected version works because the function declaration now matches its behavior.

Mistake 3: Using try! when failure is possible

try! tells Swift that you are certain no error will be thrown. If an error does occur, your program crashes at runtime.

Problem: This code force-tries an operation that can realistically fail, so the app may terminate unexpectedly.

let score = try! parseScore(from: "hello")
print(score)

Fix: Use do-catch or try? unless you are absolutely sure the operation cannot fail.

if let score = try? parseScore(from: "hello") {
    print(score)
} else {
    print("Invalid score")
}

The corrected version works because the failure path is handled safely instead of crashing the program.

Mistake 4: Confusing throws with optional returns

Beginners often try to use throws and optional return values for the exact same failure reason, which can make APIs unclear.

Problem: This function returns an optional and also throws for the same invalid input situation, so callers must deal with two overlapping failure models.

func findPositiveNumber(in text: String) throws -> Int? {
    if text.isEmpty {
        throw ParsingError.invalidNumber
    }

    return Int(text)
}

Fix: Choose one main failure model for the specific problem. If invalid input is an error, return a non-optional value and throw on failure.

func findPositiveNumber(in text: String) throws -> Int {
    guard let number = Int(text), number > 0 else {
        throw ParsingError.invalidNumber
    }

    return number
}

The corrected version works because the function communicates failure in one clear, consistent way.

7. Best Practices

Use specific error types

Specific error types make your APIs easier to understand and easier to handle correctly. Avoid vague, catch-all errors when your code can communicate something more precise.

enum FileValidationError: Error {
    case emptyFilename
    case unsupportedExtension
}

This is better than using a single generic error for every failure case because callers can react differently to each case.

Use guard for early failure

When a throwing function has required conditions, guard makes the failure path clear and keeps the main logic easier to read.

enum UsernameError: Error {
    case tooShort
}

func validateUsername(_ username: String) throws {
    guard username.count >= 3 else {
        throw UsernameError.tooShort
    }

    print("Username is valid")
}

This style reduces nesting and makes the success path stand out.

Reserve try! for truly guaranteed success

There are rare cases where try! is acceptable, but only when failure would indicate a programmer mistake rather than normal runtime input.

enum StaticDataError: Error {
    case invalid
}

func loadFixedValue() throws -> Int {
    return 100
}

let value = try! loadFixedValue()
print(value)

Even here, many teams still prefer safer alternatives. Use try! sparingly and intentionally.

Use throws when the caller needs the reason

If the exact reason for failure matters, throwing is often better than returning nil.

enum DiscountError: Error {
    case expiredCode
    case invalidCode
}

func applyDiscount(code: String) throws -> Int {
    if code == "OLD10" {
        throw DiscountError.expiredCode
    }

    guard code == "SAVE10" else {
        throw DiscountError.invalidCode
    }

    return 10
}

This gives the caller enough detail to show a helpful message or take a different action.

8. Limitations and Edge Cases

9. Practical Mini Project

Let’s build a small input validation function for a command-line style registration flow. It will check username and age, throw specific errors, and handle them with readable messages.

enum RegistrationError: Error {
    case emptyUsername
    case usernameTooShort
    case underage
}

func createAccount(username: String, age: Int) throws -> String {
    guard !username.isEmpty else {
        throw RegistrationError.emptyUsername
    }

    guard username.count >= 3 else {
        throw RegistrationError.usernameTooShort
    }

    guard age >= 18 else {
        throw RegistrationError.underage
    }

    return "Account created for \(username)"
}

do {
    let message = try createAccount(username: "Ava", age: 21)
    print(message)
} catch RegistrationError.emptyUsername {
    print("Please enter a username.")
} catch RegistrationError.usernameTooShort {
    print("Username must have at least 3 characters.")
} catch RegistrationError.underage {
    print("You must be at least 18 years old.")
} catch {
    print("Unexpected registration error.")
}

This mini project shows a realistic pattern: define an error enum, throw precise errors from validation rules, and handle each case clearly at the call site.

10. Key Points

11. Practice Exercise

Practice creating and calling your own throwing function.

Expected output: For a valid temperature, print a success message such as Temperature is valid: 25. For an invalid one, print a clear error message.

Hint: Remember that the function declaration needs throws, and the function call needs try.

enum TemperatureError: Error {
    case belowAbsoluteZero
}

func checkTemperature(_ value: Int) throws -> String {
    if value < -273 {
        throw TemperatureError.belowAbsoluteZero
    }

    return "Temperature is valid: \(value)"
}

do {
    let message = try checkTemperature(25)
    print(message)
} catch TemperatureError.belowAbsoluteZero {
    print("Temperature cannot be below absolute zero.")
} catch {
    print("Unknown temperature error.")
}

12. Final Summary

Swift throwing functions give you a clear and structured way to represent operations that can fail. By adding throws to a function, using throw for failure cases, and calling the function with try, you make error handling visible and intentional.

In this article, you saw how to declare throwing functions, return values from them, handle errors with do-catch, and use alternatives like try?, try!, and rethrows. You also saw common mistakes, best practices, and a small working project that puts the pattern into context.

A strong next step is to learn how custom error types, do-catch matching, and Result compare to throwing functions so you can choose the best error-handling style for each Swift API you write.