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

You can define your own control structure:


func ignorethrow(@noescape block: () throws -> Void) {
    do {
        try block()
    } catch {
    }
}


which you can then use like this:


ignorethrow { try fileManager.removeFileAtPath(cachePath1) }
ignorethrow { try fileManager.removeFileAtPath(cachePath2) }
ignorethrow { try fileManager.removeFileAtPath(cachePath3) }


Converting the error to an optional is probably not ideal, since the reason for throwing an error (or in the Objective-C case, returning an error) is to be more detailed in the error reporting.


EDIT: added @noescape to make it more convenient if the code references self implictly.

We also have all these annoying

catch {}
everywhere when using
NSFileManager
.


What about this solution?

extension NSFileManager {
     func tryRemoveItemAtPath(path: String) -> Bool {
          do {
               try removeItemAtPath(path)
               return true
          }
          catch {
               return false
          }
     }
}


And then:

fileManager.tryRemoveItemAtPath(cachePath1)
fileManager.tryRemoveItemAtPath(cachePath2)
fileManager.tryRemoveItemAtPath(cachePath3)

+1


The suggested work-arounds are fine, but this is such a common use case that it should be streamlined.

Yes, I have also thought about the need for an operator "try?". Just did not comment


It's not crazy to me, it is to even obvious: Not all errors need to be addressed in all contexts.


And also if "try?" is be implemented, it would be possible to disable empty catch ("catch {}") for safety and readability.

The point is, you SHOULD care. You should be striving to write error free code

not ignoring errors. Anything less is just sloppy at best, lazy at worse.

In theory: In reality not all errors are important.

And the "try?" syntax is better than a void catch {} like most people do.

Just look at some codes in my desk, using a simple search, i can see 80 error points, and 8 with catch {}. And why? Because an optional result in this point is everything i want in this particular code.


For example: Some API may throw an error parsing a JSON object or just return a void result and no error be throw. The throw version is good right? But sometimes does matter. On Swift for example, Int(String) returns a optional Int, but if we want to know why the string parser fail? We cant! And most of the time, in this case, does matter!

You can't really force the dev make something on error, and the try? will only remove the inevitable catch {} in some cases.

In practice, not all errors are even errors.


Consider this use case: You have a directory that may or may not exist, that you want to put some files in. If it doesn't exist, you want to create it first. So, you do this:


NSURL *cacheDirURL = ...

if (![cacheDirURL checkResourceIsReachableAndReturnError:NULL]) {
  [[NSFileManager defaultManager] createDirectoryAtURL:cacheDirURL withBlah:blah:blah:];
}

// now put some stuff in the directory


If the directory doesn't exist, that's not an errorin this case. It's just informational. In Swift 2, you'll have to create a Boolean variable, set it to true in the do block, set it to false in the catch block, and then branch on it (you could, of course, just create the directory in the catch block, but that just feels wrong; creating the directory is *not* error-handling code).

So what you probably should do is to try and create the directory first, without even checking if it exists first. 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.


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.


Since I have a background in file systems, I'm particularly aware of programmers ignoring errors in that realm, especially the return value from closing a file.

1. I like that Swift makes it hard for you to ignore an error.


2. I definitely agree that there needs to be a way to bridge from errors to optionals, in a way that doesn't require arbitratily pushing variables into higher scopes. Whether this is 'try?' or 'guard try' or 'try … else' or something else doesn't seem to matter too much, but there should be a direct way of doing it.


I kinda like the idea of "try…else", where you can basically catch an error from a specific try without having to enclose it in a scope, and you can do recovery that knows where you are in that scope. That could be in addition to a "catch" block at the end, which does more general cleanup. It's the scoping that's the real problem with what we've got now.


(But this is just vague hand-waving. I haven't tried to think it through to see if something like this is really sensible.)


3. It could be argued that the API for removeFileAtPath is not well designed for Swift-style error handling, because it doesn't distinguish between the cases "deleted it" vs "didn't need to delete" vs "couldn't actually delete". Similarly, as you say, best practices say that you should try to create the directory and see what happens, since checking first for its existence leaves a race condition. So, that API really needs "created it" vs "didn't need to create it" vs "couldn't actually create it".


In Obj-C, the NSError** parameter mechanism makes it easier to work around this lack, so I think my suggestion would be an extension to NSFileManager that provides Swift-error-handling-compatible API that distinguishes between the three cases.

NSURL.checkResourceIsReachable() is a VERY good use case for this, thanks. I should have used it in my sample code to prevent the discussion digressing into a file systems best practice debate..


A lot of API in Foundation were designed to return Bool values and provide NSError as an OPTIONAL source for additional information. It just feels wrong that if I only need the Bool result as before I now also have to work around the do-catch syntax.

I do not condone ignoring errors, at worse we dump errors to our logs. But some errors are much expected that logging them are useless, such as in my example code where calling removeFileAtPath() on a non-existent file is guaranteed to fail but such failure is ZERO relevance to my app.


Wether the language enforces error-handling or not, programmers who have a bad practice of ignoring important errors will find a way to do them (in Swiift's case, they'll write catch {}).


The point is, which errors to handle is the programmer's decision and the language should be able to let us express either. They already gave us try! which lets us ignore errors, but to the extent that the whole application just gives up if an error does occur.

I understand that we can do workarounds for now, but I believe Swift will benefit having it built into the language.


By the way, you can also extend and overload your ignorethrow function to revert the return types as bridged from Objective-C:

func ignorethrow(@noescape block: () throws -> Void) -> Bool {
    do {
        try block()
        return true
    } catch {
        return false
    }
}

func ignorethrow(@noescape block: () throws -> T) -> T? {
    do {
        return try block()
    } catch {
        return nil
    }
}

Thats far too general a statement to make. What constitutes an "error" depends on the API designer, but in general an error occurs when a method fails to perform all of its intended actions. Whether or not you can/must/should handle an error depends on the specific failing method and how and why you are calling it.


Swift has a sensible default behaviour - that methods should always succeed, and if not then you should handle that explicitly. They accept that sometimes there is no way to reasonably handle a failure, so we have try!. The same logic goes in reverse - there are times where there is no need to handle a failure, so it follows we should have a try? keyword.


If they don't add it, people will just hack around it with shims like the above examples. It's better that they bring it in to the language - it could present optimisation opportunities and there may be sensible limitations they can impose for safety.

The following is just an argument about the optimization opportunities.


The Swift compiler (and I include LLVM in that) has already managed to make complicated deductions from code that is not obviously optimizable. Before byteswapping was built into the language, an expression like this:


((arg & 0xFF) << 24) | ((arg & 0xFF00) << 8) | ((arg >> 8) & 0xFF00) | ((arg >> 24) & 0xFF)


was optimized into one single x86 instruction (bswap). From what I understand, the most important thing for the optimizer is that the knowledge about the code's use is as complete as possible. The compiler could inline the closure, and then optimize it just as well as a built in statement. It doesn't now, however. But when I tried my ignorethrow code, the compiler already called the closure statically, so I suppose it has enough information for inlining, and therefore further optimizations.

Yes, in your case, ignoring the error is correct, since you don't care if it succeds or not. However, regarding error handling with Bool, I pointed to a case(mkdir) where the error/no error return value seems to give you the information you need, but actually doesn't. This needs to be carefully designed so that we avoid these problems in our code.


I don't want to make this about file systems, but it already was, and I'm not comfortable pointing out subtle error handling problems in, for example, UIKit, although I'm sure they exist.