In Swift, the try?
keyword converts a call that may throw an error into an optional value. It returns a successful value (.some(T)
) or nil (.none
). Using try?
enables you to incorporate throwing code into guard
statements, allowing you to break the error handler chain and leave scope, and into conditional binding for success-specific clauses.
The merits and costs of try?
are numerous. It allows you to create more predictable code, especially in completion blocks and playgrounds, but discards errors which are almost always of value to the developer.
A while ago, I created a version of try?
that prints. It offered a balance between the desire to retain error information and the need to use optional-based control statements.
Over time, I’ve been tweaking away at this, evolving and refining the idea to get to a point where I could reproduce the overall behavior of both try?
and its fatal-error sibling try!
while retaining error-handling features. I call this multi-purposed alternative attempt
.
My attempt
function passively picks up context where a throwing statement executes and provides a default printing error-handler. If you set crashOnError
to true
, the attempt acts like try!
and aborts execution, but only after printing the error that caused this and the source location where the problem originated.
The newest component of attempt
is a custom error handler, the one that defaults to printing. It’s a little overkill, but I like the idea that I can customize the behavior as needed. If you just want to add clean-up and then print, you can call the default handler from the custom one.
An attempt
call looks like this in standalone use, with a trailing closure:
attempt { let mgr = NSFileManager.defaultManager() try mgr.createDirectoryAtPath( "/Users/notarealuser", withIntermediateDirectories: true, attributes: nil) }
But requires a more traditional argument approach when combined with guard
calls that cannot distinguish the trailing closure from their required else
clause.
guard let fileContents = attempt(closure: { _ -> [NSURL] in let url = NSURL(fileURLWithPath: "/Users/notarealuser") let mgr = NSFileManager.defaultManager() return try mgr.contentsOfDirectoryAtURL( url, includingPropertiesForKeys: nil, options: []) }) else { fatalError("failed") }
The code for attempt follows below or you can visit the wider-scoped CoreError.swift implementation on Github, which adds (updated) contextualization utilities as well. The screaming snake case constants will change soon as the modernized debug identifiers are adopted into Swift.
As always, if you find any issues or have any suggestions, please let me know.
7 Comments
Great job Erica, you made a great little tool there and ammunition to the “do is for do-while” campaign ;). Well, if implemented as part of the language the internal do could be replaced by any internal non necessarily pretty name and the outside code could freely call attempt. Also, ugliness in terms of guard could be addressed better.
Do you think we could turn this into a proposal?
I’ve brought it up on-list before (twice, I think) with zero interest. Maybe three times if you include the common default contextualizable error thing (https://github.com/erica/SwiftUtility/blob/master/Sources/CoreError.swift). Even more if you look at the variety of error literal enhancement discussions I’ve poked my nose into.
I would love for a proposal to happen but I’d be leery of moving forward unless you could grab the interest of at least one or two core team members.
Are you willing to help drum up some interest!?
Yes, I am willing to help drum up interest to help make a successful proposal. Unsure how to grab their attention, but a way can be found :).
How to grab attention: Easiest way to start is to post on the Swift Evolution mailing list, with a very clear subject line and a motivating paragraph.
This is pretty cool! I would prefer using an `assert` instead of `fatalError` though, so it doesn’t crash in production.
A try! will crash in production and any guard statement must be guaranteed to leave scope. This call can be used in top level statements as well as within scopes, and there’s no way to safely leave a top-level scope.
ah ok, that makes sense to me, good point.