Swift try, try?, and try! Explained with Examples
Swift uses try, try?, and try! when you call a throwing function. These three forms look similar, but they behave very differently: one requires you to handle errors, one converts errors into nil, and one crashes if an error happens. Understanding the difference is essential for writing safe, predictable Swift code.
Quick answer: Use try when you want to handle or propagate errors normally, try? when a failure should simply become nil, and try! only when you are absolutely sure the call cannot fail at runtime.
Difficulty: Beginner
Helpful to know first: You will understand this better if you know basic Swift functions, optionals, and how if statements and constants work.
1. What Is Swift try, try?, and try!?
In Swift, some functions are marked with throws. That means the function can either return a normal value or stop by throwing an error. When you call one of these functions, Swift makes you be explicit about how you want to deal with the possible failure.
- try calls a throwing function and requires real error handling.
- try? calls a throwing function and turns any thrown error into an optional result.
- try! calls a throwing function and assumes it will never fail; if it does fail, your program crashes.
- These forms only apply to functions or expressions that can throw.
- They are part of Swift's compile-time safety system, so Swift prevents you from ignoring possible errors accidentally.
This topic is often confused with optionals and force unwrapping. The key difference is that try? handles a thrown error by returning an optional, while try! does not return an optional just because of the !; instead, it force-assumes success and crashes on failure.
2. Why try Variants Matter
These three forms matter because different failures should be handled in different ways.
For example, if you load a configuration file that must exist for the app to work, you might use normal try and stop with a clear message if it fails. If you are attempting an optional operation, such as parsing a value that may or may not be present, try? may be a good fit. If you are calling something that you know cannot fail in the current context, try! can make code shorter, but it should be rare.
Choosing the wrong form can lead to several problems:
- You may hide an important error by using try? too casually.
- You may crash your app by using try! when failure is possible.
- You may write awkward code if you use plain try when a simple optional result would be clearer.
3. Basic Syntax or Core Idea
Defining a throwing function
First, here is a simple throwing function. It returns a username unless the input is empty.
enum ValidationError: Error {
case emptyName
}
func validatedName(_ name: String) throws -> String {
if name.isEmpty {
throw ValidationError.emptyName
}
return name
}This function is marked with throws, so callers must use one of the Swift try forms.
Using plain try
Use plain try when you want to handle the error properly.
do {
let name = try validatedName("Taylor")
print(name)
} catch {
print("Validation failed: \(error)")
}The do-catch block catches any error thrown by the function.
Using try?
Use try? when you want failure to become an optional value.
let name = try? validatedName("")
print(name as Any)If the function throws, name becomes nil instead of stopping with an error.
Using try!
Use try! only when failure is impossible or already guaranteed not to happen.
let name = try! validatedName("Morgan")
print(name)This works if the function succeeds. If it throws, the program stops immediately with a runtime crash.
4. Step-by-Step Examples
Example 1: Handling a thrown error with do-catch
This example shows the standard way to call a throwing function and respond to failure.
enum PasswordError: Error {
case tooShort
}
func checkPassword(_ password: String) throws -> String {
if password.count < 8 {
throw PasswordError.tooShort
}
return "Accepted"
}
do {
let result = try checkPassword("secret123")
print(result)
} catch {
print("Password check failed: \(error)")
}Use this style when the reason for failure matters and you want to keep that information.
Example 2: Using try? for optional parsing
Sometimes a failure is not exceptional. In that case, turning a thrown error into nil makes the code simpler.
enum ParseError: Error {
case invalidNumber
}
func parseWholeNumber(_ text: String) throws -> Int {
guard let number = Int(text) else {
throw ParseError.invalidNumber
}
return number
}
let age = try? parseWholeNumber("42")
let badAge = try? parseWholeNumber("forty-two")
print(age as Any)
print(badAge as Any)The first result is an optional containing 42. The second result is nil. This is useful when you only care whether the operation succeeded.
Example 3: Using try! when success is guaranteed
This example shows the narrow case where try! can be acceptable.
enum TextError: Error {
case empty
}
func firstCharacter(of text: String) throws -> Character {
guard let character = text.first else {
throw TextError.empty
}
return character
}
let letter = try! firstCharacter(of: "Swift")
print(letter)This works because the string literal is clearly non-empty. The risk is low here, but in general try! should be used sparingly.
Example 4: Propagating errors from another throwing function
You do not always handle errors immediately. Sometimes your function should pass them upward.
enum LoginError: Error {
case missingUsername
}
func validateUsername(_ username: String) throws -> String {
if username.isEmpty {
throw LoginError.missingUsername
}
return username
}
func normalizedUsername(from input: String) throws -> String {
let username = try validateUsername(input)
return username.lowercased()
}
do {
let name = try normalizedUsername(from: "Admin")
print(name)
} catch {
print("Login problem: \(error)")
}Here, the inner function uses plain try because it wants to pass the error upward rather than converting or force-assuming success.
5. Practical Use Cases
- Use try when validating user input and you need to tell the user exactly what went wrong.
- Use try when loading important app data where failure must be handled explicitly.
- Use try? when a failed conversion should simply produce no result, such as optional parsing or best-effort reads.
- Use try? when you want concise code in an if let or guard let chain.
- Use try! in tests, prototypes, or cases where the input is fully controlled and failure would indicate a programming mistake.
- Use plain try inside another throwing function when you want to propagate the error to the caller.
6. Common Mistakes
Mistake 1: Calling a throwing function without try
A throwing function must be called with one of Swift's try forms. Beginners often forget this when they first see throws.
Problem: Swift will report an error like Call can throw, but it is not marked with 'try' and the error is not handled because the code ignores the function's failure path.
enum FileError: Error {
case missing
}
func loadFileName() throws -> String {
throw FileError.missing
}
let name = loadFileName()Fix: Add try and either catch the error or propagate it.
do {
let name = try loadFileName()
print(name)
} catch {
print("Could not load file name: \(error)")
}The corrected version works because it explicitly acknowledges that the function can fail.
Mistake 2: Using try? when the error information matters
try? is convenient, but it silently discards the actual error. That can make debugging and user feedback much harder.
Problem: This code turns every failure into nil, so you cannot tell whether the problem was missing data, invalid input, or something else.
enum AccountError: Error {
case locked
case missingUser
}
func fetchAccountName(for id: Int) throws -> String {
throw AccountError.locked
}
let name = try? fetchAccountName(for: 42)
print(name as Any)Fix: Use plain try with do-catch when the cause of failure should be preserved.
do {
let name = try fetchAccountName(for: 42)
print(name)
} catch AccountError.locked {
print("The account is locked.")
} catch AccountError.missingUser {
print("No user was found.")
} catch {
print("Unexpected error: \(error)")
}The corrected version works because it keeps the error information available for specific handling.
Mistake 3: Using try! when failure is possible
try! is the riskiest form. It looks short and convenient, but it will crash if the function throws.
Problem: If the input is invalid, this code causes a runtime trap instead of handling the failure safely.
enum NumberError: Error {
case negative
}
func squareRoot(of value: Int) throws -> Double {
if value < 0 {
throw NumberError.negative
}
return Double(value).squareRoot()
}
let result = try! squareRoot(of: -9)
print(result)Fix: Use plain try and catch the error, or use try? if losing the error details is acceptable.
do {
let result = try squareRoot(of: -9)
print(result)
} catch {
print("Cannot calculate square root: \(error)")
}The corrected version works because it handles the error path instead of force-assuming success.
7. Best Practices
Prefer try as the default choice
In most real applications, errors are meaningful. Starting with plain try keeps the code honest and preserves error details.
do {
let username = try validatedName("Avery")
print(username)
} catch {
print("Validation error: \(error)")
}This approach is easier to maintain because future failures remain visible and debuggable.
Use try? only for truly optional results
If failure is expected and should simply mean “no value,” try? is a clean option.
let number = try? parseWholeNumber("100")
if let number = number {
print("Parsed: \(number)")
} else {
print("No valid number provided.")
}This is a good fit because the code only needs success or absence, not detailed error reasons.
Avoid try! in production paths unless failure is impossible
Even if try! looks safe today, later code changes can make it unsafe. Keep it for tightly controlled situations.
// Acceptable only because the input is fully known here.
let letter = try! firstCharacter(of: "Hello")
print(letter)Use this style only when the failure condition is impossible in practice and obvious to future readers.
8. Limitations and Edge Cases
- try? discards the original error, so you cannot inspect what failed later.
- try! crashes at runtime if the function throws, which makes it dangerous for user-controlled input.
- If a function is not marked throws, you do not use any try form with it.
- Inside a throwing function, plain try can propagate errors upward without do-catch.
- Using try? changes the result type into an optional, which may require additional unwrapping.
- A common “not working” scenario is forgetting that try? can hide why a value became nil.
9. Swift try vs try? vs try!
Because these three forms are commonly confused, it helps to compare them directly.
| Form | What happens on success | What happens on failure | Typical use |
|---|---|---|---|
| try | Returns the normal value | Throws an error that must be caught or propagated | Default choice when error details matter |
| try? | Returns an optional containing the value | Returns nil | Optional or best-effort operations |
| try! | Returns the normal value | Crashes the program | Rare cases where failure is impossible |
A simple rule is: start with try, switch to try? only when nil is the right failure result, and use try! only when you can prove the call will never throw.
10. Practical Mini Project
This mini project validates a promo code entered by the user. It shows all three forms in a small, complete example.
enum PromoCodeError: Error {
case emptyCode
case invalidCode
}
func validatePromoCode(_ code: String) throws -> String {
if code.isEmpty {
throw PromoCodeError.emptyCode
}
let allowedCodes = ["SAVE10", "FREESHIP"]
guard allowedCodes.contains(code) else {
throw PromoCodeError.invalidCode
}
return code
}
// 1. Standard error handling with try
do {
let validCode = try validatePromoCode("SAVE10")
print("Accepted code: \(validCode)")
} catch {
print("Promo code error: \(error)")
}
// 2. Optional handling with try?
let optionalCode = try? validatePromoCode("BADCODE")
print("Optional result: \(optionalCode as Any)")
// 3. Forced success with try!
let knownCode = try! validatePromoCode("FREESHIP")
print("Known valid code: \(knownCode)")This example shows the three variants side by side. In a real app, the first version is usually the safest, the second is useful for optional flows, and the third should be limited to guaranteed-success inputs.
11. Key Points
- try is the standard form for calling a throwing function.
- try? converts a thrown error into nil.
- try! force-assumes success and crashes if an error is thrown.
- Use plain try when the reason for failure matters.
- Use try? when failure should simply mean “no value.”
- Use try! only in tightly controlled situations where failure is impossible.
- A common compiler error is forgetting to mark a throwing call with try.
- try? changes the result into an optional, so you may need to unwrap it.
12. Practice Exercise
Build a throwing function called positiveNumber(from:) that:
- Takes a String input.
- Throws an error if the string cannot be converted to an Int.
- Throws an error if the number is less than or equal to zero.
- Returns the valid positive number otherwise.
- Call it once with plain try inside do-catch.
- Call it once with try?.
Expected output: One successful result and one optional nil for invalid input.
Hint: Create an enum that conforms to Error and use guard statements.
enum PositiveNumberError: Error {
case notANumber
case notPositive
}
func positiveNumber(from text: String) throws -> Int {
guard let number = Int(text) else {
throw PositiveNumberError.notANumber
}
guard number > 0 else {
throw PositiveNumberError.notPositive
}
return number
}
do {
let value = try positiveNumber(from: "25")
print("Valid number: \(value)")
} catch {
print("Error: \(error)")
}
let optionalValue = try? positiveNumber(from: "-5")
print("Optional result: \(optionalValue as Any)")13. Final Summary
Swift's try variants let you choose how a throwing function should behave at the call site. Plain try is the normal and safest default because it keeps errors visible. try? is useful when failure should quietly become nil. try! is the most dangerous option because it crashes if anything goes wrong.
If you remember one rule, make it this: use try first, use try? when an optional result is the right model, and use try! only when failure is truly impossible. A good next step is to learn deeper Swift error handling patterns such as custom error types, do-catch matching, and rethrowing functions.