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.
- A function with throws can use throw to send an error.
- Calling a throwing function requires try.
- You usually handle errors with do-catch.
- throws belongs in the function declaration, while throw is the statement that actually sends an error.
- This is different from optionals: optionals represent missing values, while throwing functions represent actual failure conditions that deserve explanation.
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:
- make failure explicit at the call site
- let you provide specific error information
- help avoid silent failures
- work naturally with Swift APIs and standard error handling patterns
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:
- throws: marks a function as capable of throwing an error
- throw: actually sends an error from inside the function
- try: marks a call to a throwing function
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
- Validating user input such as email addresses, passwords, or age checks.
- Parsing strings into numbers, dates, or custom data types.
- Reading files or external data where the operation may fail.
- Building utility functions that should stop immediately when invalid data is detected.
- Writing reusable APIs where callers need detailed failure reasons.
- Creating wrapper functions around other throwing operations using rethrows.
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
- throws does not replace optionals. Use optionals for absence of value, and errors for meaningful failure states.
- You cannot ignore thrown errors unless you convert them with try? or force them with try!.
- A throwing function does not have to throw every time; it only declares that it can.
- If a closure can throw, the surrounding function may also need throws or rethrows.
- rethrows is limited: the function may only throw when one of its function parameters throws.
- Using try? hides the original error information, which can make debugging harder when failures matter.
- A common “not working” case is adding try but forgetting to wrap the call in a context that actually handles the error, such as do-catch or another throwing function.
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
- throws marks a function that may fail with an error.
- throw sends an error from inside a throwing function.
- try is required when calling a throwing function.
- do-catch is the standard way to handle thrown errors.
- try? converts errors into nil.
- try! crashes if an error is thrown, so use it very carefully.
- rethrows is used when a function only throws because one of its closure arguments throws.
- Specific error types make your code easier to understand and maintain.
11. Practice Exercise
Practice creating and calling your own throwing function.
- Create an enum named TemperatureError with a case named belowAbsoluteZero.
- Write a function named checkTemperature that takes an Int.
- If the temperature is less than -273, throw the error.
- If the value is valid, return a message string.
- Call the function using do-catch and print either the result or an error message.
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.