Message Forwarding: What Happens When a Message Send Fails

Trace the full pipeline the Objective-C runtime walks through when an object receives a message it cannot immediately answer, from dynamic method resolution through fast forwarding to full forwarding, and understand why an object might deliberately want to fail 'normal' lookup in order to answer messages it never declared.

Earlier in this course, the 'Dynamic Method Resolution' lesson showed that when an object receives a message its class has no implementation for, the runtime doesn't instantly crash — it first gives the class a chance to supply a method implementation at the last possible moment, via +resolveInstanceMethod: or +resolveClassMethod:. That was only the first of three chances the runtime gives an object before it gives up and raises -doesNotRecognizeSelector:. This lesson covers the complete picture: what happens after dynamic resolution also fails, when the runtime moves on to two further stages — fast forwarding and full forwarding — collectively known as message forwarding. Message forwarding is what makes Objective-C's dynamism genuinely powerful: it lets one object stand in for another, transparently, which is the foundation of proxy objects, dynamic language bridges, and mocking frameworks used throughout the Cocoa ecosystem.

The Full Pipeline: Four Chances Before Failure

When you send a message like [someObject doSomething], the runtime's very first move is ordinary method lookup: it checks the object's class, then walks up the superclass chain, searching each class's method list for an implementation of doSomething. If it finds one, it calls it immediately, and none of what follows in this lesson ever happens — forwarding only begins once ordinary lookup has already failed completely, at every level of the class hierarchy.

Once ordinary lookup fails, the runtime does not immediately raise an exception. Instead, it offers the receiving class four successive chances to make the message succeed anyway, each one more expensive and more general than the last: dynamic method resolution, fast forwarding, full forwarding, and only after all three have declined does it fall back to -doesNotRecognizeSelector:, which by default raises an NSInvalidArgumentException and crashes the program. Understanding this order matters because each stage is a hook you can override, and the runtime always tries the cheap ones before the expensive ones.

This lesson's diagram lays out that exact order. Read it as a decision tree the runtime walks every single time a message send doesn't match a compiled-in method: first ask 'does a method exist?', and if not, work down through cheaper-to-more-expensive recovery mechanisms until one of them succeeds or all of them are exhausted.

Stage One: Dynamic Method Resolution (Recap)

The first recovery stage is dynamic method resolution, covered in depth in the earlier 'Dynamic Method Resolution' lesson, so this lesson only recaps it briefly before moving on to what comes next. When ordinary lookup fails for an instance message, the runtime calls +resolveInstanceMethod: on the receiver's class (or +resolveClassMethod: for a class-side message), passing the selector that failed. If your override uses class_addMethod to attach a real C function as the implementation for that selector and returns YES, the runtime immediately retries the original message send from scratch — and this time, ordinary lookup succeeds, because the method now genuinely exists.

The key thing to remember about this stage, for the purposes of this lesson, is what it is good for: supplying a missing implementation for the receiver's own selector, typically backed by @dynamic properties or generated accessor logic. It is not about redirecting the message to some other object — that's the job of the next two stages. If +resolveInstanceMethod: returns NO (or isn't overridden at all, which is the default), the runtime concludes that this class simply has no way to answer the selector itself, and moves on to fast forwarding.

Stage Two: Fast Forwarding with forwardingTargetForSelector:

Fast forwarding is the runtime's next, cheaper option, and it exists for a very specific and very common situation: the receiving object itself can't handle the message, but it knows of another object that can. Rather than the receiver implementing the behavior itself, it can simply redirect the entire message — selector and all arguments — to a different receiver, unchanged. This is done by overriding -forwardingTargetForSelector:, which takes the failed selector and returns the object that should receive it instead.

The runtime calls -forwardingTargetForSelector: and, if it returns a non-nil object (and not self, which would cause infinite recursion), re-sends the exact same original message — same selector, same arguments — to that returned object instead. Critically, this happens beneath the level of NSInvocation construction: no method signature needs to be looked up, no invocation object needs to be built, and no argument boxing or unboxing occurs. The message send is essentially just re-aimed at a new receiver and dispatched normally from there, which is why this path is called 'fast' — it costs barely more than an ordinary message send.

This makes fast forwarding the right first choice whenever your real goal is simple delegation to a single other object: 'if I can't handle this message, my helper object almost certainly can, so just send it to helper instead.' If the delegate object itself doesn't implement the selector either, the delegate's own lookup and forwarding machinery kicks in from there, so forwarding chains can cascade naturally across several objects.

Stage Three: Full Forwarding with methodSignatureForSelector: and forwardInvocation:

If -forwardingTargetForSelector: returns nil — meaning there is no single other object that can simply take over the whole message unchanged — the runtime falls through to the last and most powerful stage: full forwarding. Full forwarding is slower and more involved than fast forwarding because it requires two separate overrides working together, and because it requires the runtime to build a real NSInvocation object to represent the message send, rather than just re-aiming a plain message.

The first piece is -methodSignatureForSelector:, which the runtime calls to ask: 'if you were going to handle this selector, what would its argument and return types actually be?' You answer by returning an NSMethodSignature object, most commonly built from another object's own class using instanceMethodSignatureForSelector:, or constructed manually from a type-encoding string when no existing method matches exactly. Without a correct method signature, the runtime has no way to know how many bytes of arguments to read off the stack or how to interpret the return value, so this step is mandatory — if you return nil here, the runtime gives up immediately and calls -doesNotRecognizeSelector:.

Once a valid method signature comes back, the runtime uses it to build a fully-formed NSInvocation capturing the original selector, target, and every argument, and hands that invocation to -forwardInvocation:. This is where the real generality of full forwarding shows up: inside -forwardInvocation: you have a live object representing the entire pending message send, and you can inspect its selector and arguments with -selector and -getArgument:atIndex:, modify them, redirect the invocation to one target or split logic across several targets by changing its target and calling -invokeWithTarget:, or even set a custom return value with -setReturnValue:. None of that flexibility exists at the fast-forwarding stage, which is exactly why full forwarding costs more: constructing and dispatching through an NSInvocation involves real allocation and reflection-like work, whereas fast forwarding is just a redirected message send.

Why the Order Matters: Cheap Before Expensive

The reason the runtime tries these stages in this specific order — resolution, then fast forwarding, then full forwarding — is a straightforward cost tradeoff. Dynamic method resolution costs almost nothing beyond the initial failed lookup, and it's the right tool when the receiver genuinely can answer for itself, just not through a normal compiled method. Fast forwarding costs a little more, since it's an extra message send to ask for a substitute target, but avoids ever touching NSMethodSignature or NSInvocation machinery. Full forwarding is by far the most expensive of the three, since it requires querying a method signature and materializing a full invocation object before your code even runs.

In practice, this means you should implement the least powerful stage that solves your actual problem. If you're building a simple delegate-forwarding proxy that hands off every unhandled message to one other object unchanged, override -forwardingTargetForSelector: and stop there — reaching for full forwarding when fast forwarding would do just adds unnecessary overhead and two extra methods to maintain. Reserve full forwarding for cases that genuinely need to inspect or rewrite arguments, or that need to route different selectors to different targets based on more than just the selector itself.

The Real Use Case: Building a Proxy Object

The scenario that ties all three stages together is the proxy object: an object that presents itself as though it implements a large, sometimes unknown, set of methods, but actually forwards each one, behind the scenes, to one or more underlying objects it wraps. A lightweight multicast proxy is a classic example — an object that looks like a single delegate but actually broadcasts every incoming message to several real delegate objects, useful when multiple parts of an app all need to observe the same events without wiring each one up individually. A dynamic API wrapper is another: an object that stands in front of a network client or a scripting bridge and forwards arbitrary method calls through to a backing implementation that isn't fully known at compile time.

This proxy pattern is exactly what Cocoa's own NSProxy class exists for. Unlike NSObject, NSProxy implements essentially none of the normal instance methods itself — it's deliberately a nearly-empty shell whose entire purpose is to make every message it receives fall straight through to forwarding. A typical NSProxy subclass overrides -forwardingTargetForSelector: for the simple single-target case, or -methodSignatureForSelector: plus -forwardInvocation: when it needs to inspect arguments, redirect to multiple backing objects, or synthesize a return value. Because NSProxy skips all the normal NSObject method implementations, forwarding kicks in immediately for virtually every message it receives, which is precisely the behavior a transparent proxy needs.

A Worked Example: Fast Forwarding to a Helper

Consider a Logger object that wraps a lower-level FileWriter but doesn't want to reimplement every one of FileWriter's methods just to pass calls through. Rather than writing dozens of trivial pass-through methods by hand, Logger can override -forwardingTargetForSelector: to check whether its wrapped FileWriter responds to the failed selector, and if so, hand the message straight to it. From the caller's point of view, Logger appears to support the entire FileWriter interface, even though it only truly implements a handful of methods itself.

This pattern scales naturally: as FileWriter's interface grows, Logger needs no changes at all to keep forwarding new methods, because the forwarding decision is made dynamically, per selector, at the moment each message actually fails ordinary lookup — not hardcoded as a fixed list of pass-through method stubs.

Code Examples

writeLine: is never declared or implemented on Logger at all. Ordinary lookup fails, dynamic resolution isn't overridden so it declines, and then -forwardingTargetForSelector: runs, sees that self.writer responds to writeLine:, and hands the entire message off to it unchanged. This is the cheapest possible way to make one object transparently stand in for another.

if ([self.writer respondsToSelector:aSelector]) { return self.writer; }

Common mistake: Returning self from forwardingTargetForSelector: causes infinite recursion, since the runtime would just re-send the same failing message to the same object again.

MulticastProxy can't use fast forwarding, because it needs to dispatch to possibly several targets, not just one. -methodSignatureForSelector: finds a real signature from whichever target actually implements the selector, and then -forwardInvocation: walks every target, invoking the same invocation on each one that responds. This is a genuine multicast — every observer in self.targets receives the call — something fast forwarding's single-return-value design cannot express. Note that the same NSInvocation object can be safely re-invoked against multiple targets in a loop, since -invokeWithTarget: just re-points the invocation's target and dispatches, without consuming or mutating the argument data itself.

[invocation invokeWithTarget:target];

This is the baseline behavior every Objective-C object has without any forwarding overrides: all three recovery stages decline in turn, in the exact order the diagram shows, and the program terminates with an unrecognized-selector exception. Every override shown in the earlier examples is a hook inserted into this same default pipeline, not a replacement for it. Expected result: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Empty mysteryMethod]: unrecognized selector sent to instance ...'

@interface Logger : NSObject
@property (nonatomic, strong) FileWriter *writer;
@end

@implementation Logger

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.writer respondsToSelector:aSelector]) {
        return self.writer;
    }
    return [super forwardingTargetForSelector:aSelector];
}

@end

// Usage:
Logger *log = [[Logger alloc] init];
log.writer = [[FileWriter alloc] initWithPath:@"/tmp/app.log"];
[log writeLine:@"Started up"];  // Logger doesn't implement writeLine:,
                                 // so it's forwarded straight to writer.
@interface MulticastProxy : NSObject
@property (nonatomic, strong) NSArray<id> *targets;
@end

@implementation MulticastProxy

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    for (id target in self.targets) {
        NSMethodSignature *sig = [target methodSignatureForSelector:selector];
        if (sig) return sig;
    }
    return [super methodSignatureForSelector:selector];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    BOOL handled = NO;
    for (id target in self.targets) {
        if ([target respondsToSelector:invocation.selector]) {
            [invocation invokeWithTarget:target];
            handled = YES;
        }
    }
    if (!handled) {
        [self doesNotRecognizeSelector:invocation.selector];
    }
}

@end
@interface Empty : NSObject
@end

@implementation Empty
@end

Empty *e = [[Empty alloc] init];
[e mysteryMethod];
// 1. Ordinary lookup fails (no such method anywhere in the hierarchy)
// 2. +resolveInstanceMethod: not overridden -> declines (returns NO by default)
// 3. -forwardingTargetForSelector: not overridden -> returns nil by default
// 4. -methodSignatureForSelector: not overridden -> returns nil by default
// 5. -doesNotRecognizeSelector: raises NSInvalidArgumentException, crash

The Message Send Failure Pipeline

Every failed message send walks this same path: ordinary lookup, then dynamic resolution, then fast forwarding, then full forwarding — each stage more general and more expensive than the last, with -doesNotRecognizeSelector: as the final fallback if full forwarding also fails to produce a target.

Message
   ↓
Method Found?
 ├── Yes → Execute
 └── No
      ↓
Dynamic Resolution
      ↓
Resolved?
 ├── Yes → Execute
 └── No
      ↓
Fast Forwarding
      ↓
Full Forwarding

Where This Shows Up in Real Frameworks

NSProxy and its forwarding-based subclasses underpin several well-known Cocoa mechanisms: NSDistantObject uses full forwarding to make method calls transparently cross process boundaries for inter-process communication, key-value observing sometimes relies on dynamically created forwarding subclasses, and many third-party mocking and stubbing libraries used in unit testing build mock objects entirely out of -forwardingTargetForSelector: and -forwardInvocation:, letting a single generic mock class answer for any protocol without hand-writing a fake implementation of every method.

Implementing -forwardInvocation: without also overriding -methodSignatureForSelector:.

Always override both together for full forwarding — return a correct NSMethodSignature from -methodSignatureForSelector: first.

The runtime needs the method signature to construct the NSInvocation in the first place; if -methodSignatureForSelector: returns nil, -forwardInvocation: is never even called, and -doesNotRecognizeSelector: fires immediately instead.

Reaching for full forwarding (methodSignatureForSelector: + forwardInvocation:) to forward every message to one single other object.

Use -forwardingTargetForSelector: for simple single-target delegation, and reserve full forwarding for cases that truly need to inspect, modify, or multicast arguments.

Full forwarding is meaningfully more expensive, since it requires querying a signature and constructing a real NSInvocation for every call, where fast forwarding is just a redirected message send.

Returning self from -forwardingTargetForSelector: as a 'fallback'.

Return nil (or call [super forwardingTargetForSelector:aSelector]) when there is no other object to forward to, and let the pipeline continue to full forwarding.

Returning self tells the runtime to re-send the exact same failing message to the exact same object, which fails identically and recurses forever, eventually crashing with a stack overflow rather than a clean unrecognized-selector exception.

Forgetting to call [super doesNotRecognizeSelector:invocation.selector] (or an equivalent) when -forwardInvocation: can't find any target to handle a selector.

Explicitly call -doesNotRecognizeSelector: for any selector your forwarding logic can't actually satisfy.

If -forwardInvocation: silently returns without invoking anything and without raising, the caller gets back a garbage/undefined return value instead of a clear crash, which is far harder to debug than a proper unrecognized-selector exception.

In the message forwarding pipeline, which stage runs first after ordinary method lookup fails? A. Fast forwarding B. Full forwarding C. Dynamic method resolution D. doesNotRecognizeSelector:

C. Dynamic method resolution — Dynamic method resolution (+resolveInstanceMethod: / +resolveClassMethod:) is always tried first, since it's the cheapest recovery option and gives the receiver's own class a chance to supply a real implementation.

What does -forwardingTargetForSelector: return to redirect a message to another object? A. An NSInvocation B. An NSMethodSignature C. The other object itself D. A BOOL

C. The other object itself — Fast forwarding works by simply returning the substitute receiver object; the runtime then re-sends the exact original message, unchanged, to that object.

Why is full forwarding considered more expensive than fast forwarding? A. It requires network access B. It requires constructing an NSMethodSignature and a full NSInvocation object before your code runs C. It can only be used with NSProxy D. It runs on a background thread

B. It requires constructing an NSMethodSignature and a full NSInvocation object before your code runs — Full forwarding requires the runtime to query a method signature and materialize a real NSInvocation representing the entire call, which is genuinely more work than fast forwarding's simple redirected message send.

What happens if -methodSignatureForSelector: returns nil during full forwarding? A. forwardInvocation: is called anyway with a nil invocation B. The runtime falls back to fast forwarding C. The runtime immediately calls -doesNotRecognizeSelector:, and forwardInvocation: is never invoked D. The message is silently dropped

C. The runtime immediately calls -doesNotRecognizeSelector:, and forwardInvocation: is never invoked — A valid method signature is required to build the NSInvocation that forwardInvocation: would receive; without one, the runtime has no way to construct that invocation, so it gives up immediately.

Which Cocoa class is specifically designed as an almost-empty shell so that virtually every message it receives falls through to forwarding? A. NSObject B. NSProxy C. NSInvocation D. NSMethodSignature

B. NSProxy — NSProxy deliberately implements almost none of NSObject's usual methods, so ordinary lookup fails for nearly everything sent to it, sending nearly every message straight into the forwarding pipeline — ideal for building transparent proxies.

A proxy needs to broadcast one incoming message to three different underlying observer objects. Which forwarding mechanism should it use? A. Dynamic method resolution, since it's the cheapest B. Fast forwarding, returning one of the three observers C. Full forwarding, invoking the NSInvocation against each observer in turn D. It cannot be done with message forwarding at all

C. Full forwarding, invoking the NSInvocation against each observer in turn — Fast forwarding can only redirect to a single substitute object. Multicasting to several targets requires full forwarding, where forwardInvocation: can call invokeWithTarget: repeatedly against each target.

Design a ReadOnlyWrapper proxy class that wraps any object and forwards every message to it UNCHANGED, except it should refuse (raise an exception) any selector whose name starts with 'set'. Decide which forwarding stage (fast or full) you need, and explain why.

This needs full forwarding, not fast forwarding, because the decision to forward or refuse depends on inspecting the selector itself, not just the presence of a single substitute target. Override -methodSignatureForSelector: to return the wrapped object's real signature, and override -forwardInvocation: to check NSStringFromSelector(invocation.selector) for a 'set' prefix — raising an exception if it matches, and otherwise calling [invocation invokeWithTarget:self.wrapped].

Think about whether you need to inspect the selector's name before deciding whether to forward it, and whether you need only a single fixed target.