Reputation: 11529
I would like to throw an exception from some "deep" function, so it bubbles up to another function, where I want to catch it.
f1
calls f2
calls f3
calls ... fN
which may throw an error
I would like to catch the error from f1
.
I've read that in Swift I have to declare all methods with throws
, and also call them using try
.
But that's quite annoying:
enum MyErrorType : ErrorType {
case SomeError
}
func f1() {
do {
try f2()
} catch {
print("recovered")
}
}
func f2() throws {
try f3()
}
func f3() throws {
try f4()
}
...
func fN() throws {
if (someCondition) {
throw MyErrorType.SomeError
}
}
Isn't there a similar concept to the RuntimeException
in Java, where throws
doesn't leak all the way up the call chain?
Upvotes: 27
Views: 18009
Reputation: 32093
Isn't there a similar concept to the
RuntimeException
in Java, wherethrows
doesn't leak all the way up the call chain?
Swift does indeed have error handling which doesn't propegate at compile time.
However, before I discuss those, I must say that the one you point out, where you use the language's do...catch
, try
, throw
, and throws
keywords/features to handle errors, is by far the safest and most preferred. This ensures that every single time an error might be thrown or caught, it's handled correctly. This completely eliminates surprise errors, making all code more safe and predictable. Because of that inherent compile- and run-time safety, you should use this wherever you can.
func loadPreferences() throws -> Data {
return try Data(contentsOf: preferencesResourceUrl, options: [.mappedIfSafe, .uncached])
}
func start() {
do {
self.preferences = try loadPreferences()
}
catch {
print("Failed to load preferences", error)
assertionFailure()
}
}
guard let fileSizeInBytes = try? FileManager.default.attributesOfItem(atPath: path)[.size] as? Int64 else {
assertionFailure("Couldn't get file size")
return false
}
Probably the easiest way to silence Swift's compiler is with try!
- this will allow you to use native Swift errors, but also ignore them.
Here's what your example code would look like with that:
enum MyErrorType : ErrorType {
case SomeError
}
func f1() {
f2()
}
func f2() {
f3()
}
func f3() {
try! f4()
}
...
func fN() throws {
if (someCondition) {
throw MyErrorType.SomeError
}
}
Obviously, this has the problem of not allowing you to ever catch these, so if you want a silent error you can catch, read on.
There are also assertion
s, precondition
s, and fatalError
s, which I described in detail in my answer from October of 2017. The compiler provides reasonable handling of these, such as ensuring that return statements and other control flow are placed and omitted when appropriate. Like try!
, however, these cannot be caught.
exit
[BSD] & abort
[BSD] are in this family if your goal is to stop the program immediately.
If you venture outside Swift into the wider Apple ecosystem (that is, if you are writing Swift on an Apple platform), you also see Objective-C's NSException
. As you desire, this can be thrown by Swift without using any language features guarding against that. Make sure you document that! However, this cannot be caught by Swift alone! You can write a thin Objective-C wrapper that lets you interact with it in the Swift world.
func silentButDeadly() {
// ... some operations ...
guard !shouldThrow else {
NSException.raise(NSExceptionName("Deadly and silent", format: "Could not handle %@", arguments: withVaList([problematicValue], {$0}))
return
}
// ... some operations ...
}
func devilMayCare() {
// ... some operations ...
silentButDeadly()
// ... some operations ...
}
func moreCautious() {
do {
try ObjC.catchException {
devilMayCare()
}
}
catch {
print("An NSException was thrown:", error)
assertionFailure()
}
}
Of course, if you're writing Swift in a Unix environment, you still have access to the terrifying world of Unix interrupts. You can use Grand Central Dispatch to both throw and catch these. And, as you desire, there's no way for the compiler to guard against them being thrown.
import Dispatch // or Foundation
signal(SIGINT, SIG_IGN) // // Make sure the signal does not terminate the application.
let sigintSource = DispatchSource.makeSignalSource(signal: SIGINT, queue: .main)
sigintSource.setEventHandler {
print("Got SIGINT")
// ...
exit(0)
}
sigintSource.resume()
exit
[BSD] is in this family if your goal is to trap it and read its code.
Upvotes: 0
Reputation: 32093
To elaborate on Максим Мартынов's answer, Swift has 3 ways to do throw undeclared, uncatchable errors (but other approaches are possible if you want to venture outside Swift's standard library). These are based on the 3 levels of optimization:
-Onone
: No optimization; debug build-O
: Normal optimization; release build-O SWIFT_DISABLE_SAFETY_CHECKS
: Unchecked optimization; extremely optimized buildassertionFailure(_:)
Write this line when you're doing debugging tests and hit a line you don't think should ever be hit. These are removed in non-debug builds, so you must assume they will never be hit in the production app.
This has a sister function called assert(_:_:)
, which lets you assert at runtime whether a condition is true. assertionFailure(_:)
is what you write when you know the situation is always bad, but don't think that'll harm the production code very much.
if color.red > 0 {
assertionFailure("The UI should have guaranteed the red level stays at 0")
color = NSColor(red: 0, green: color.green, blue: color.blue)
}
preconditionFailure(_:)
Write this line when you're sure some condition you've described (in documentation, etc.) was not met. This works like assertionFailure(_:)
, but in release builds as well as debug ones.
Like assertionFailure(_:)
, this one's got a sister function called precondition(_:_:)
, which lets you decide at runtime whether a precondition was met. preconditionFailure(_:)
is essentially that, but assuming the precondition is never met once the program gets to that line.
guard index >= 0 else {
preconditionFailure("You passed a negative number as an array index")
return nil
}
Note that, in extremely optimized builds, it is not defined what happens if this line is hit! So if you don't want your app to wig out if it might ever hit this, then make sure the error state is handleable.
fatalError(_:)
Used as a last resort. When every other attempt to save the day has failed, here is your nuke. After printing the message you pass to it (along with the file and line number), the program stops dead in its tracks.
Once the program gets to this line, this line always runs, and the program never continues. This is true even in extremely optimized builds.
#if arch(arm) || arch(arm64)
fatalError("This app cannot run on this processor")
#endif
Further reading: Swift Assertions by Andy Bargh
Upvotes: 15
Reputation: 1227
Yes, it is possible!
Use: fatalError("your message here")
to throw runtime exception
Upvotes: 47
Reputation: 4702
The error handling mechanism in Swift does not involve raising unchecked (runtime) exceptions. Instead, explicit error handling is required. Swift is certainly not the only recently designed language to go for this design – for instance Rust and Go also in their own ways also require explicitly describing the error paths in your code. In Objective-C the unchecked exception feature exists, but is largely used only for communicating programmer errors, with the notable exception of a few key Cocoa classes such as NSFileHandle
which tends to catch people out.
Technically you do have the ability to raise Objective-C exceptions in Swift with the use of NSException(name: "SomeName", reason: "SomeReason", userInfo: nil).raise()
as is explained in this excellent answer to this question, arguably a duplicate of your question. You really shouldn't raise NSExceptions though (not least because you have no Objective-C exception catching language feature available to you in Swift).
Why did they go with this design? Apple's "Error Handling in Swift 2.0" document explains the rationale clearly. Quoting from there:
This approach […] is very similar to the error handling model manually implemented in Objective-C with the NSError convention. Notably, the approach preserves these advantages of this convention:
- Whether a method produces an error (or not) is an explicit part of its API contract.
- Methods default to not producing errors unless they are explicitly marked.
- The control flow within a function is still mostly explicit: a maintainer can tell exactly which statements can produce an error, and a simple inspection reveals how the function reacts to the error.
- Throwing an error provides similar performance to allocating an error and returning it – it isn’t an expensive, table-based stack unwinding process. Cocoa APIs using standard NSError patterns can be imported into this world automatically. Other common patterns (e.g. CFError, errno) can be added to the model in future versions of Swift.
[…]
As to basic syntax, we decided to stick with the familiar language of exception handling. […] by and large, error propagation in this proposal works like it does in exception handling, and people are inevitably going to make the connection.
Upvotes: 9