
In early stages of feature development or experimentation, it’s common to reach for custom error types—usually enums conforming to Error. While this is good practice for production-quality code, it can introduce unnecessary friction when you're simply trying to validate a concept or build quickly.
Swift and Foundation offer a robust set of built-in error types, like NSError and CocoaError, which are often good enough for many prototyping and internal use cases. By using these existing types, you can iterate faster, write less boilerplate, and still maintain meaningful error information.
This post outlines how to use these built-in tools effectively, showcasing several examples.
Use NSError with Custom User Info
Instead of creating a new error enum, use NSError to express rich context:
throw NSError(domain: NSCocoaErrorDomain, code: NSFileReadCorruptFileError, userInfo: [
NSLocalizedDescriptionKey: NSLocalizedString("Could not read file", comment: "Read error description"),
NSLocalizedFailureReasonErrorKey: NSLocalizedString("File was in an invalid format", comment: "Read failure reason")
])
This is especially helpful when you want to capture meaningful error messages with minimal setup.
Define Project-Specific Domains (Without Custom Types)
Custom domains let you organize errors without requiring new types:
throw NSError(domain: "ListDocumentDomain", code: -1, userInfo: [
NSLocalizedDescriptionKey: NSLocalizedString("Could not archive list", comment: "Archive error description"),
NSLocalizedFailureReasonErrorKey: NSLocalizedString("No list presenter was available for the document", comment: "Archive failure reason")
])
This allows for namespacing while staying lean during prototyping.
Leverage CocoaError for Standard Semantics
When a more semantic error type is useful, CocoaError is a good middle ground between NSError and custom types:
throw CocoaError(.formatting, userInfo: [
NSDebugDescriptionErrorKey: "Cannot parse \(value). Expected format like \"\(formatStyle.format(3.14))\""
])
This helps maintain consistency while still being descriptive.
Another common case is guarding against missing file references:
guard let imageURL = subject.imageURL else {
throw CocoaError(.fileNoSuchFile, userInfo: [
NSDebugDescriptionErrorKey: "Cannot lookup image URL for \(subject)."
])
}
Readable, expressive, and quick to implement.
Why This Helps During Prototyping
- Reduces time spent defining boilerplate
- Keeps code flexible for fast iteration
- Enables meaningful error messages with low effort
- Easy to transition to custom types later
For internal tools, temporary utilities, or proof-of-concept features, this approach keeps development speed high without sacrificing clarity.
Learn from SwiftLang's Open-Source Codebase
Many other error handling examples can be found SwiftLang’s open-source repositories. Exploring real-world implementations like this can be incredibly valuable for developers. By studying open-source code, you can:
- Discover idiomatic patterns used by experienced teams
- Understand pragmatic decisions made in real-world scenarios
- Build stronger instincts for solving problems and designing clean solutions
Taking time to read through repositories like SwiftLang's not only sharpens your skills but also provides insights into how foundational libraries handle common concerns. Whether you're prototyping or refining production code, these lessons can help you write better, more maintainable software.
Takeaway
Swift’s built-in error types are often all you need to build, test, and iterate quickly. Reach for them when:
- You need meaningful context but not full-blown domain models
- You're writing internal features, tooling, or MVPs
- You're experimenting and want to avoid over-engineering
Custom error types still have their place—especially in libraries or public interfaces—but deferring that step can often keep you moving faster.
References
- Apple Developer Documentation: Swift.Error