Async Digital / Iris
Iris internals

When a deep link points at nothing

Existence

A URL is a name, and a name can outlive the thing it names. A link can arrive for a route a newer build no longer defines, a URL whose identifier its destination rejects, or a screen nothing is waiting on. Iris does not treat that as an error to trap after the fact. It makes “is the target there?” a single optional-valued lookup, so absence is an ordinary value the code has to handle, and a link to nothing stops cleanly instead of opening an empty screen.

Abstract

Every deep link is a promise about a destination, made by whoever wrote the URL and kept, or not, by the app that receives it. Between the two sits a gap: the named thing may not exist. Most apps discover this late, halfway into a push, and the user lands on a blank detail screen or a spinner that never resolves.

Iris closes the gap by refusing to let absence be implicit. The lookups that can come up empty return an Optional, not a bare value, and the type system makes every caller confront the empty case at one gate. This piece explains the pattern, the single lookup that embodies it (the handoff registry’s handoff(for:), which returns a Handoff?), and why the guarantee holds even as the app grows.


The problem

A name the target may not keep

A deep link names a destination. The name is minted in one place (a share sheet, a notification, a saved Shortcut, a test, an agent) and honoured in another (the running app), and time passes in between. By the time the link is opened, the conversation it points at may be gone, the identifier may be stale, or the URL may name a route a newer build has dropped. The name is a claim about the world, and the world does not have to agree.

The unsafe way to meet that claim is to trust it. Force-unwrap the lookup, assume the row is there, push the screen, and read the record it needs once you have already arrived. When the record is missing, the failure surfaces late and badly: a detail screen with nothing in it, a sheet over an empty list, or a crash on the force-unwrap. The link technically worked. It just opened onto nothing.

The trouble is not that targets go missing. They always will. It is that absence gets discovered too late to handle gracefully. The fix is to move the check to the front and make it impossible to skip. Iris does that with one idea applied everywhere a lookup can come up empty: the lookup returns an Optional, and the Optional is the gate.


Mechanism

Absence is a value, not an accident

The clearest instance is the handoff registry. Iris keeps a small store of pending handoffs (the mechanism that hands a URL’s payload to the view about to appear) keyed by route. The registry is a dictionary, and asking it for a route’s handoff returns an Optional: a Handoff when one is registered, nil when none is. Exactly one method answers the question, and its return type, Handoff?, carries both possible answers in the open.

Because the answer is an Optional and not a bare value, no caller can use it without first deciding what to do when it is empty. The registry’s own callers show the shape: they bind the result with if let, take the present branch when a handoff is waiting, and fall to a defined branch when it is not. The empty case is not an exception thrown from somewhere deep. It is the other half of a value the caller is already holding, and Swift will not let them reach the handoff until they have separated the two.

Fig 1 One lookup, an Optional answer. handoff(for:) returns either a handoff or nil, and the return type makes the caller handle both before it can touch the handoff.

The same shape guards the front door. Turning a URL into an intent is also an optional-valued lookup: parsing returns an intent when the URL matches a known route, and nil when the URL names no route, or when a matched route’s own rule rejects the identifier the URL carries. That is the partial URLPathRouter layer beneath the total URL codec: at the router, a URL that names no route produces no intent, and because the router only ever routes an intent it actually has, there is nothing to dispatch. The link does not half-open a screen. It resolves to nothing and stops. A codec built over the router can map that miss to a fallback intent instead, so whether an unrecognised URL stops here or lands on a fallback is the codec’s call, not the router’s; the URL-to-intent seam takes up that choice.

Absence is not an error the code traps after the fact. It is one of the two values the lookup was always going to return.


Why it holds

The check is not optional to run

Three properties keep the gate honest as the app grows.

None of this rests on remembering to be careful. The optional return is a wall, not a sign. It is there on the first call and on the thousandth, and it is there for the route someone adds next year without having read this page.


Cost

The gate answers presence, not meaning

Nothing here is free, and it is worth being exact about what the gate does not do.

What the gate leaves to the caller

The lookup answers one question, present or absent, and no more. It does not decide what an absence should mean, because that depends on where you are asking. For the handoff registry, nil means “nothing is registered yet, so register a fresh one and deliver into it.” For URL parsing, nil means “this names nothing, stop.” The same empty value, opposite conclusions.

So the meaning of nil lives at the call site, not in the gate. That is deliberate: the registry stays a small, reusable store that knows about presence and nothing about policy, which is what lets one type serve stack routes and sheet routes without change. The price is that you cannot read the meaning of an absence from the gate alone. You have to read the caller to know whether an empty result is a routine “not yet” or a dead end.

That trade is the right one for infrastructure. A gate that also decided meaning would have to know every caller’s intent, and it would stop being reusable the moment two callers wanted different things from an empty result. Keeping the gate plain (present or absent, one lookup, an honest Optional) is what lets absence be handled correctly in each place while being defined in exactly one.

The consumer’s-eye view of the same boundary, where a tapped control and an external link meet the one resolver, is in the parity study. The rest of how Iris is built lives on the Iris internals index, and the capability it serves is mapped on the deep-linking hub.