Can we get a "try?" as a more lenient "try!"?

I'm impressed by the structure imposed by the new do-catch construct.

I also appreciate that the Swift team gave us try! to handle cases where we are sure that errors won't occur.

Now one case that keeps bugging me when refactoring to Swift 2.0 code is I have to write something like this:

let fileManager = NSFileManager.defaultManager()
do {
   try fileManager.removeFileAtPath(cachePath1)
} catch _ { }

do {
   try fileManager.removeFileAtPath(cachePath2)
} catch _ { }

do {
   try fileManager.removeFileAtPath(cachePath3)
} catch _ { }


I just want to delete those paths if possible, but I don't care if they fail; they may have not existed in the first place and if they did, the system will clean the Caches sooner or later.

If I write this as

do {
   try fileManager.removeFileAtPath(cachePath1)
   try fileManager.removeFileAtPath(cachePath2)
   try fileManager.removeFileAtPath(cachePath3)
} catch _ { }

then if cachePath1 fails, 2 and 3 will never be executed. And while we don't care if the deletes fail, not trying at all just feels wrong. What if each method I call have a side effect that I need to trigger?


On the other hand,

try! fileManager.removeFileAtPath(cachePath1)
try! fileManager.removeFileAtPath(cachePath2)
try! fileManager.removeFileAtPath(cachePath3)

will crash my app if any of the deletes fail. Definitely not what I want.


Now I don't know if someone else had the same idea before, but what if we have a try? as well to counterpart the try!? We can write

try? fileManager.removeFileAtPath(cachePath1)

which ignores any thrown errors, and more importantly, does not trap on error.


For methods that return an object, Swift 2.0 already bridges Objective-C methods of the pattern

- (SomeObject *)doSomethingAndReturnError:(NSError **)error;


to

func doSomething() throws -> SomeObject


such that doing

let object: SomeObject = try! doSomething()

returns a non-optional SomeObject instance.


We can then make "try?" return an optional:

let object = try? doSomething() // object will be bound to SomeObject?


For void methods, returning a Void? is fine, or we can borrow the bridging rules from Objective-C and just return a Bool instead.

if try? fileManager.removeFileAtPath(cachePath1) {
   println("file deleted!")
}


Is this idea too crazy or am I missing something that will break this kind of syntax?



Update:

I hope the discussion does not digress into a debate wether to handle file-system errors or not; I just used removeFileAtPath as ONE of the many use cases we encountered this problem. CharlesS pointed out another good use case: methods that were primarily used to check for a state, such as NSURL.checkResourceIsReachable()

Accepted Reply

Looks like "try?" has been added to Beta 6:


http://adcdownload.apple.com/Developer_Tools/Xcode_7_beta_6/Xcode_7_beta_6_Release_Notes.pdf

Replies

So what you probably should do is to try and create the directory first, without even checking if it exists first.


Well, no. If the directory already exists, and I try to create it, I'm going to get an error which once again, is not an error. I just want the folder to be there, so if it already is, then great! Not a problem. Now, I could just ignore the error on folder creation, but then the problem is that in the case that the folder didn't already exist, now suddenly I do care about the error. I could do a checkURLIsReachable() after trying to create the folder, but then I'll get the wrong error — I don't want to know that the folder doesn't exist, I want to know why I couldn't create it.


Anyway, either way this is going to require the ignoring of some error somewhere.


If it is important for some reason to check first, do that, but remember that the directory might appear between your call to check for existence and you actually trying to create it.


If we're talking about something in ~/Library/Caches/<my app bundle id>, that's pretty unlikely. Even more so if the app is sandboxed.


So what is my point? The point is that you have to do error checking on the creation part, but only selectively. One of the errors that directory creation might return (directory already exists) should be ignored in this case. However, any other error has to be handled, because the error might be that you have insufficient privileges, the disk might be full, the disk might have disappeared, the directory above might have disappeared/not exist in the first place, and so on.


Yes, of course, that's the other option, to try to create the directory, and then check the error object to see what the error was. So, just check for NSCocoaErrorDomain and NSFileWriteFileExistsError. Aaaaand also check for NSPOSIXErrorDomain and EEXIST. And also check for NSOSStatusErrorDomain and the five or six error codes in there that look like "already exists" errors. And also invent a time machine, travel to the future to see what new ErrorType Apple's going to eventually replace NSError with, and check for that too, because this isn't future-proof at all. Think I'm being paranoid about this? Well, if you look at the docs for NSFileWriteFileExistsError, you'll see that it was introduced in 10.7. So code that was written for Snow Leopard would have been checking against EEXIST, and it would have completely silently failed once Lion rolled around and started giving a new error that the developer couldn't possibly have known about. Actually, code for Snow Leopard wouldn't have been checking against EEXIST, because I just checked, and what that OS version actually did was to return NSFileWriteUnknownError, with a second error object containing EEXIST being put in its NSUnderlyingErrorKey. So there's another thing you have to check for. Uggggg


Checking NSError codes is not reliable at all unless the API docs explicitly say what codes they will return, which not only isn't true for Apple's APIs, but they don't even stay consistent from one OS release to the next. This makes checking NSError codes tantamount to using private APIs. They're clearly for display only.

So, if what you are saying about the APIs is correct (and I have no reason to believe otherwise), the discussion from a file management point of view (and maybe other points of view as well) is moot, since the APIs that actually return errors with NSError do so in a way that makes it very hard to handle them in a correct way. However, this kind of error handling (did something/didn't have to/error) has to be handled in some way. Ignoring specific errors is fine, but except for cases such as the original question (removing cache files that would be cleaned later anyway), ignoring all errors is normally a bad idea.


I'm not going to comment on the specific file system practices, since the original poster doesn't want me to, and that would be a long thread in itself.

Ignoring specific errors is fine, but except for cases such as the original question (removing cache files that would be cleaned later anyway), ignoring all errors is normally a bad idea.


A syntax for ignoring specific errors is all that's being asked for.

No, the "try?" suggestion is ignoring all errors for a call.

For a specific call. There are lots of specific cases where you don't need the error.

I'll say it once more: Ignoring specific errors is fine, but except for cases such as the original question (removing cache files that would be cleaned later anyway), ignoring all errors is normally a bad idea.


There are very few cases where ignoring all types of errors for a call is a good idea. You can ignore errors that you know are not going to affect the behaviour of your program, but by definition unknown errors are not in that category. You might think you don't need the error, though.

Do you see the irony in the fact that even in your absolute statement, you acknowledge a case where it makes perfect sense to ignore the error?

I have not argued against ignoring errors completely in some cases, so I don't know where you get that from. I have argued that ignoring the type of error completely normally means that you assume that it is a certain type of error, and when the real error comes along, your code will behave incorrectly.

There are a lot of cases where you don't carewhat the error is, and what's important is whether the file is reachable or whether you can perform some operation or not.


Here's a couple more, in addition to the ones already mentioned:


- Setting the color of a view object representing a file in the project, dependent on whether the file is reachable or not. For something like Xcode's left-hand sidebar, does it matter if the file is unreachable because it's missing, or because you don't have read permission to its parent folder? Not really, you just want to color it red to show that you can't open it.


- Closing and/or releasing some kind of resource, whether it's a file descriptor, a network socket, or whatever. Like the temp files, it'll be closed when your app exits anyway, and its failure to close won't affect your processing of whatever data you just read from it without errors.


- Implementing a delegate method, or overriding a method, or doing something with a method whose signature looks something like -isSomeCertainKindOfFileAtURL:, with no error parameter. If you can't open the file handle to the file, you're just returning that no, this is not the file you were looking for, as it's all that really matters (and, really, all you can do).


- Compiling a list of files that may be in any of several standard directories, where not all of the directories are required to exist.


There are cases where it's appropriate to ignore the error, and a try? operator would facilitate that.

I would posit that Swift is correct as it is, but the FileManager API is what needs to be changed.


It sounds like we need `NSFileManager { func ifExistsDeleteFile(file:string) -> Void` or something

As shown at WWDC, Apple divides errors into 3 categories:


  1. Trivial errors

    Int(someString), etc.

  2. Detailed, recoverable errors
    • File not found
    • Network failure
    • User cancellation, etc.
  3. Logic errors

    Assertions, overflows, NSException, etc.


The presenter says explicitly: "Int(someString) can fail, but an optional return type is fine", and in swift ErrorHandler is about the category 2.

The Optionals will help trivial errors, and logic errors should crash you app, and you must fix this.


I'm ok with that! Except that lack the operator "try?"! Let me explain:


Who said i did not need to know in more detail why Int(someString) failed?


That was a developer arbitrary decision based on own experience: In most cases, you just need to know that the parser failed and deal with it. Optionals are more than enough in this case.


And I always can "promote" an error of category 1 to category 2 or even 3.

// category 1
if let a = Int("1") {

}



// category 2
enum DataError : ErrorType {
    case ParserError
}
extension String {
    func toInt() throws -> Int {
        guard let a = Int("1") else {
            throw DataError.ParserError;
        }
        return a;
    }
}
do {
let a = try "1".toInt()
//...
} catch {
//...
}


// category 3
let a = Int("a")!
print("This string number is: \(a)")


At the same time, we have the ability to do the reverse:


//1
extension Array {
    subscript (s index: Int) -> T? {
        return self.indices ~= index ? self[index] : nil
    }
}
//2
enum DataError : ErrorType {
    case OutOfBounds
}
extension Array {
    func atIndex(index: Int) throws -> T {
        guard let response = self[s: index] else {
            throw DataError.OutOfBounds
        }
        return response
    }
}
let arr = [1,2,3]
let c = arr[s: 5]; // possible error category 1
let b = try arr.atIndex(6); // possible error category 2
let a = arr[7]; // possible error category 3


The ability to change how important an error is has always been on the developer's responsibility, is an explicit agreement between him and the compiler because it formally established the functions for such.


We already have the try! operator to promote some code made on "category 2" to 3 code. But we dont have to make the category 2 to 1. Its possible make some hacks, but is better make this built in on the language.


If we look some samples codes from Apple, scattered throughout developer library we will see that in several places when they pass nil as the argument to a call NSError function, and this is not anything crazy: I repeat, some errors messages are irrelevant in some applications.


And in several cases we have to deal with a developer arbitrary decision to provide a more detailed response to a problem:

// Swift 1.2
if let unarchiveJSON = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: nil) as? [String:NSObject] {
   // This is all i need, no detail is important to me in this app, in another app maybe will.
}


But now:


var unarchiveJSON:[String:NSObject]?
do {
    unarchiveJSON = try NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments) as? [String:NSObject]
} catch { }
if let unarchiveJSON = unarchiveJSON {
// ...
}


Using "try?"

if let unarchiveJSON = try? NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments) as? [String:NSObject] {
   // This is all i need in this particular case example.
}


The try? operator is valid in this context as any other explicit treatment. Is me, developer saying: In this context, that error is not of category 2 but category 1.

This new proposed operator, will simply normalize the inevitable fact that in many cases some errors messages are not important, using a unified, clear syntax for everyone. And it will also, maybe, help to handle with the case "catch {}" making this a compilation error (error-prone). Optionals result, is, in that way, some kind of error handler! I'm not avoid the error, just making another treatment, with less boilerplate.


And most importantly: Will encourage more people to detail their errors using "throws" without fear of creating problems for those who use your code.


With try? operator, Int(someString) could throws some error and this:


if let a = try? Int("1") {
//...
}


Will be used for people who dont care why this Int parser fail, like i don't care why NSJSONSerialization fail above.

A try? operator would be excellent.


In practice, I've started moving away from failable initializers for throwing initializers for exactly the reason you mentioned, I can actually provide the caller the reason it failed.


The other great thing about try?, it's easily auditable.


Have you logged a bug for this?

Already requested as rdar://21692462 with a link to this thread.

No, it is not. Wether you act on an error or not, is up to you. How you handle the error, is not.

Try! is a lame lazy way of abdicating responsibility. You ask for an action, you need to deal

with the consequences of that request. If it fails and you really don't care, fine, ignore it or better

yet, set a flag so some other method knows what and why something did or did not function. Windows(tm)

has billions of errors in its code. Most of them, are ignored. Do you really want to write that kind of code?

Think of a neural net. Errors in such a construct could prove fatal, not just to software but to the users

of that code. Consider the SSL and like supposedly secure entities. By all means, write lazy code if it

gets you through the day. Personally, I'd rather never write a single line of code if I could not be sure

it would produce the same result, every time it executed.

You do not know, at run time wether that file exists or not so, NO, it is NOT guaranteed to fail. If

the file exists, your call will remove it. If it does not, you know it does not unless, you ignore the

error. This is so horribly basic, I fail to understand how so many here don't see it.