doesNotRecognizeSelector:
Understand doesNotRecognizeSelector: as the final, capstone stage of the complete Objective-C message-dispatch failure pipeline -- the point reached only after lookup, dynamic method resolution, fast forwarding, and full forwarding have all been given a chance and all declined -- and learn to read the resulting NSInvalidArgumentException, override doesNotRecognizeSelector: for diagnostics, and use respondsToSelector: defensively to avoid reaching it in the first place.
Every message send that fails does not fail immediately. Objective-C gives a receiving object a long, ordered sequence of chances to handle an unrecognized selector before the program actually crashes, and this lesson is about the very last link in that chain: doesNotRecognizeSelector:. It only matters, and only makes sense, in the context of everything that comes before it -- the sibling Dynamic Method Resolution lesson covers resolveInstanceMethod: and resolveClassMethod:, and the sibling Message Forwarding lesson covers forwardingTargetForSelector: (fast forwarding) and methodSignatureForSelector:/forwardInvocation: (full forwarding). This lesson ties all of that together into one pipeline, ending at the method that finally gives up and raises an exception, and shows how to override that final method for diagnostics and how to avoid reaching it at all with respondsToSelector: checks.
The Complete Message-Dispatch Failure Pipeline
When code sends a message to an object -- [obj doSomething] -- and that object's class has no method matching the selector anywhere in its own method list or up its superclass chain, Objective-C does not immediately crash. It instead walks the receiver through a fixed, ordered sequence of escalating opportunities to supply an implementation anyway, and only after every one of those opportunities has been offered and declined does the runtime finally raise an exception.
That full pipeline, reproduced below, is the actual subject of this lesson. It begins the instant a message is sent, and each stage hands off to the next only on failure -- any stage that succeeds short-circuits the rest of the pipeline entirely and the message completes as if nothing unusual happened.
This lesson is deliberately the capstone of that pipeline: it assumes the sibling Dynamic Method Resolution and Message Forwarding lessons for the mechanics of the middle stages, and focuses on the final stage those two lessons lead up to -- what happens when every earlier stage has already failed.
• A failed message send is not instant failure -- it is an ordered sequence of escalating recovery opportunities. • Each stage in the pipeline can fully resolve the message and short-circuit everything after it. • doesNotRecognizeSelector: is reached only when every earlier stage has declined to handle the message. • This lesson is the capstone tying together Dynamic Method Resolution and Message Forwarding, covered in their own sibling lessons.
The Pipeline, Stage by Stage
The diagram accompanying this lesson lays the pipeline out as a single vertical chain: Message, then Lookup, then Dynamic Resolution, then Fast Forwarding, then Full Forwarding, then doesNotRecognizeSelector:, and finally Exception. Every arrow in that chain represents a real handoff -- the runtime does not skip stages, and it does not retry a stage twice.
Message is the ordinary send -- [obj someSelector] compiling down to a call to objc_msgSend with the receiver and selector as its first two arguments. Lookup is the runtime searching the receiver's class's method cache, and on a miss, its method list and superclass chain, for a matching implementation -- covered in full in the objc_msgSend in Depth lesson. Everything from Dynamic Resolution onward in this chain only happens because Lookup has already failed to find anything.
Dynamic Resolution is the receiver's class being asked, via +resolveInstanceMethod: or +resolveClassMethod:, whether it wants to supply an implementation for this exact selector right now, on the fly -- the subject of the sibling Dynamic Method Resolution lesson in full. Fast Forwarding is the receiver being asked, via -forwardingTargetForSelector:, whether some other object entirely should simply receive this message instead. Full Forwarding is the last and most general opportunity -- -methodSignatureForSelector: describing the message's shape, followed by -forwardInvocation: doing arbitrary work with the fully reified NSInvocation. Both fast and full forwarding are covered in complete depth in the sibling Message Forwarding lesson; this lesson treats them as the two stages that stand between Dynamic Resolution and final failure.
• Message -> Lookup -> Dynamic Resolution -> Fast Forwarding -> Full Forwarding -> doesNotRecognizeSelector: -> Exception, in that exact order. • Lookup: the ordinary cache/method-list/superclass-chain search, detailed in the objc_msgSend in Depth lesson. • Dynamic Resolution: +resolveInstanceMethod: / +resolveClassMethod:, covered fully in the Dynamic Method Resolution lesson. • Fast Forwarding (-forwardingTargetForSelector:) and Full Forwarding (-methodSignatureForSelector: + -forwardInvocation:), covered fully in the Message Forwarding lesson.
Why Each Stage Gets a Real Chance Before the Next Runs
The ordering of the pipeline is not arbitrary -- it moves from cheapest and most specific to most expensive and most general, and each stage is a genuinely separate opportunity, not a fallback path that silently reuses the previous stage's failure. Dynamic Method Resolution runs first among the recovery stages because it is the cheapest: it lets a class lazily supply a method implementation for a selector it knows about in advance, without touching message routing at all, and once class_addMethod succeeds inside resolveInstanceMethod:, the runtime re-attempts the original lookup as if the method had always existed.
If dynamic resolution declines (by returning NO, or not overriding the resolve method at all), the runtime tries fast forwarding next, because redirecting the entire message to a different object is cheap compared to reifying the message into a full NSInvocation. Only when even that is declined does the runtime pay the real cost of full forwarding -- building an NSInvocation object, which requires an accurate NSMethodSignature exactly as the sibling Objective-C Type Encodings lesson describes -- because forwardInvocation: is the most powerful and most expensive stage, capable of arbitrary argument manipulation and multi-target delegation.
Only once forwardInvocation:'s default implementation is reached without being overridden -- or an override explicitly declines by calling super -- does the runtime finally give up and call doesNotRecognizeSelector:. Every one of these stages is a real method call on the receiver, not a conceptual simplification; you can override every one of them, and this lesson's own examples show a doesNotRecognizeSelector: override alongside a dynamic-resolution and fast-forwarding example specifically to make that concrete.
• The stages run cheapest-to-most-expensive: dynamic resolution first, then fast forwarding, then full forwarding. • Each stage is a real, overridable method call on the receiver -- not an internal fallback the runtime handles invisibly. • Dynamic resolution succeeding causes the runtime to re-attempt the original lookup, as if the method had existed all along. • doesNotRecognizeSelector: is reached only when the default (unoverridden, or super-calling) forwardInvocation: is what ultimately runs.
doesNotRecognizeSelector: and NSInvalidArgumentException
doesNotRecognizeSelector: is itself an ordinary Objective-C instance method, declared on NSObject, that the runtime calls automatically as the very last stage of the failure pipeline once forwarding has fully declined to handle the message. Its default implementation, inherited from NSObject, does exactly one thing: it raises an NSInvalidArgumentException, and it is that raised exception -- not doesNotRecognizeSelector: being called in the first place -- that actually terminates the program if nothing catches it.
The resulting crash message is one of the most recognizable in all of Objective-C debugging: '-[ClassName someSelector]: unrecognized selector sent to instance 0x1007041a0'. Every piece of that message is meaningful for debugging -- the class name and selector identify exactly which message failed and on what type, and the hexadecimal address identifies the specific object instance that received it, which is often the fastest way to distinguish 'this class never implements this method at all' from 'this one particular instance somehow ended up as the wrong type or already deallocated.'
Because doesNotRecognizeSelector: is an ordinary, overridable instance method rather than a hardcoded runtime trap, it is possible to override it -- either to add custom diagnostics before the crash, or, in narrow and generally discouraged cases, to swallow the failure entirely by not calling super and returning some default value instead. The example below shows the diagnostic use, which is the pattern actually seen in production code and debugging tooling.
• doesNotRecognizeSelector: is an ordinary NSObject instance method, called automatically as forwarding's last resort. • Its default implementation raises NSInvalidArgumentException -- the exception is what actually crashes the program. • The classic crash message '-[ClassName someSelector]: unrecognized selector sent to instance 0x...' names the class, selector, and the exact failing instance's address. • Because it is an ordinary overridable method, it can be overridden for diagnostics before (or, rarely and riskily, instead of) crashing.
Overriding doesNotRecognizeSelector: for Diagnostics
The most common legitimate reason to override doesNotRecognizeSelector: is to log extra context -- which object, which selector, what state the object was in -- before the crash happens, which is especially valuable in cases where the default crash log alone doesn't make the root cause obvious (for example, a selector arriving from a dynamically-constructed string via performSelector:, where the source of the wrong selector isn't visible at the call site in a stack trace).
The pattern is always the same: override doesNotRecognizeSelector:, do whatever logging or diagnostic work is useful, and then call [super doesNotRecognizeSelector:anInvocation] (or, in the classic non-invocation-taking override shown below, [super doesNotRecognizeSelector:aSelector]) so the exception still actually gets raised. Swallowing the call entirely without calling super and without raising anything yourself leaves the program in an undefined state -- the original message send effectively returns garbage, since no implementation ever actually ran -- which is why this technique is used for diagnostics layered on top of the crash, not as a way to avoid the crash.
This override point is also exactly where infrastructure code -- crash reporters, test harnesses that want more informative failure messages, or mocking frameworks -- hooks in to capture more detail than the bare system crash log would otherwise provide, which is covered further as a real-world use in the example below.
• Overriding doesNotRecognizeSelector: is the standard place to add diagnostic logging just before a crash. • Always call super's implementation afterward so the actual NSInvalidArgumentException still gets raised. • Never silently swallow the failure without super or your own exception -- the caller receives an undefined/garbage result. • Crash reporters, test harnesses, and mocking frameworks commonly use this override to capture richer diagnostic context.
Defensive respondsToSelector: Checks as Prevention
The best way to handle an unrecognized-selector failure is, of course, to never reach the pipeline's final stage at all, and the standard defensive tool for that is -respondsToSelector:, which asks an object directly whether it implements (or, via dynamic resolution or forwarding, will end up correctly handling) a given selector, returning a plain BOOL rather than triggering the failure pipeline.
This is the idiomatic guard for optional protocol methods -- a delegate protocol commonly declares several @optional methods, and calling code checks [delegate respondsToSelector:@selector(optionalCallback)] before sending that message, exactly because an optional method genuinely might not be implemented by every conforming object, and sending it unconditionally would walk straight into this lesson's entire failure pipeline and, absent forwarding, crash.
respondsToSelector: is not a full substitute for understanding the pipeline, though -- it reflects what the runtime believes right now, and a class using dynamic resolution or forwarding can make respondsToSelector: return YES for a selector that has no ordinary method list entry at all, by overriding respondsToSelector: itself alongside the resolution/forwarding methods. Used correctly, though, it remains the simplest and cheapest way to avoid ever reaching doesNotRecognizeSelector: in the overwhelming majority of ordinary code.
• -respondsToSelector: asks whether an object handles a selector, returning BOOL instead of triggering the failure pipeline. • The idiomatic guard for @optional delegate/protocol methods, which genuinely may not be implemented by every object. • A class using dynamic resolution or forwarding can (and should) override respondsToSelector: to stay consistent with what it will actually handle. • It is the cheapest, most common way to avoid reaching doesNotRecognizeSelector: in ordinary application code.
Code Examples
With none of the pipeline's recovery stages overridden, every single one declines in sequence and the runtime reaches doesNotRecognizeSelector:'s default implementation, which raises NSInvalidArgumentException with the classic message identifying the class, selector, and exact failing instance address.
// reason: '-[Widget explode]: unrecognized selector sent to instance 0x1007041a0'
Dynamic Resolution, covered fully in the sibling Dynamic Method Resolution lesson, is the pipeline's first recovery stage. Because it succeeds here -- class_addMethod installs a real implementation and returns YES -- the runtime re-attempts the original lookup, finds the newly-added method, and the message completes normally; Fast Forwarding, Full Forwarding, and doesNotRecognizeSelector: are never reached at all.
class_addMethod([self class], sel, (IMP)explodeIMP, "v@:");
This example assumes Dynamic Resolution was NOT overridden (or declined), so the pipeline reaches Fast Forwarding next -- covered fully in the sibling Message Forwarding lesson. Returning a non-nil object from -forwardingTargetForSelector: redirects the whole message cheaply, without building an NSInvocation, and Full Forwarding and doesNotRecognizeSelector: are never reached.
return _detonator; // redirect the ENTIRE message, cheaply This is the standard pattern for adding diagnostics at the pipeline's final stage: log whatever extra context is useful -- here the class, selector name via NSStringFromSelector, and the object's own debugDescription -- and then always call super's implementation so the real NSInvalidArgumentException still gets raised. This is exactly the kind of hook crash reporters and test harnesses use to capture more detail than the bare system crash log provides.
[super doesNotRecognizeSelector:aSelector];
Common mistake: Overriding doesNotRecognizeSelector: and forgetting to call super (or raise an exception of your own): the message send then silently 'succeeds' with an undefined/garbage result instead of crashing, which is far harder to debug than the crash it was meant to avoid.
Because -widgetWillExplode: is declared @optional, not every WidgetDelegate-conforming object implements it. Guarding the send with -respondsToSelector: avoids ever entering the failure pipeline for delegates that simply don't implement the optional method, which is the idiomatic, cheapest way to prevent reaching doesNotRecognizeSelector: in the first place, as opposed to reacting to it after the fact.
if ([self.delegate respondsToSelector:@selector(widgetWillExplode:)]) {
@interface Widget : NSObject
@end
@implementation Widget
@end
Widget *w = [[Widget alloc] init];
[w explode];
// Lookup fails (no -explode anywhere in Widget's class or NSObject).
// Dynamic Resolution declines (no +resolveInstanceMethod: override).
// Fast Forwarding declines (no -forwardingTargetForSelector: override).
// Full Forwarding declines (no -forwardInvocation: override).
// doesNotRecognizeSelector: runs, raising NSInvalidArgumentException:
//
// *** Terminating app due to uncaught exception 'NSInvalidArgumentException',
// reason: '-[Widget explode]: unrecognized selector sent to instance 0x1007041a0'void explodeIMP(id self, SEL _cmd) {
NSLog(@"%@ exploded, dynamically resolved!", self);
}
@implementation Widget
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(explode)) {
class_addMethod([self class], sel, (IMP)explodeIMP, "v@:");
return YES; // handled -- Lookup is re-attempted and now succeeds
}
return [super resolveInstanceMethod:sel];
}
@end
[w explode]; // "Widget exploded, dynamically resolved!" -- no crash at all@interface Detonator : NSObject
- (void)explode;
@end
@implementation Detonator
- (void)explode { NSLog(@"Detonator handled it via fast forwarding!"); }
@end
@implementation Widget {
Detonator *_detonator;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(explode)) {
return _detonator; // redirect the ENTIRE message, cheaply
}
return [super forwardingTargetForSelector:aSelector];
}
@end@implementation Widget
- (void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"[DIAGNOSTIC] %@ received unhandled selector: %@ -- last known state: %@",
NSStringFromClass([self class]),
NSStringFromSelector(aSelector),
[self debugDescription]);
// Always call super so the real exception is still raised -- this
// override ADDS diagnostics, it does not suppress the failure.
[super doesNotRecognizeSelector:aSelector];
}
@end
[w explode];
// Logs the diagnostic line first, THEN crashes with the usual
// NSInvalidArgumentException, exactly as it would have without the override.@protocol WidgetDelegate <NSObject>
@optional
- (void)widgetWillExplode:(Widget *)widget;
@end
@implementation Widget
- (void)triggerExplosion {
if ([self.delegate respondsToSelector:@selector(widgetWillExplode:)]) {
[self.delegate widgetWillExplode:self];
}
// Actual explosion logic continues here regardless of the delegate.
}
@endThe Complete Message-Dispatch Failure Pipeline
Every stage after Lookup is a genuine, overridable opportunity to handle an otherwise-unrecognized message -- Dynamic Resolution and Forwarding are covered in their own sibling lessons; this lesson covers the final two steps, where every earlier opportunity has already declined.
Message
↓
Lookup
↓
Dynamic Resolution
↓
Fast Forwarding
↓
Full Forwarding
↓
doesNotRecognizeSelector:
↓
ExceptionWhere This Pipeline Actually Matters in Practice
The 'unrecognized selector sent to instance' crash is one of the single most common Objective-C runtime crashes reported in production crash logs, and it almost always traces back to one of three causes: a genuinely missing method (a typo'd selector, or a method that was removed but is still called from somewhere), an object of the wrong type receiving a message meant for a different class (common with loosely-typed id variables or incorrectly-cast objects from collections), or a use-after-free where the address in the crash log belongs to deallocated, reused memory that no longer holds the object it appears to. Reading the crash message's class name, selector, and instance address carefully -- exactly as this lesson breaks down -- is usually the fastest way to distinguish which of those three it is, and an overridden doesNotRecognizeSelector: with extra logging, as shown above, is a common technique for narrowing that down further in hard-to-reproduce cases.
• (void)doesNotRecognizeSelector:(SEL)aSelector { NSLog(@"Ignoring unknown selector: %@", NSStringFromSelector(aSelector)); // no call to super, no exception raised } - (void)doesNotRecognizeSelector:(SEL)aSelector { NSLog(@"Ignoring unknown selector: %@", NSStringFromSelector(aSelector)); [super doesNotRecognizeSelector:aSelector]; }
Omitting the call to super does not make the failed message send safe -- it makes it silent. The original caller still receives a garbage or zeroed return value, since no real implementation ever ran, and the actual programming error (a genuinely missing method) is now hidden instead of surfaced as a crash with a clear stack trace.
[self.delegate widgetWillExplode:self]; // sent unconditionally to an @optional method if ([self.delegate respondsToSelector:@selector(widgetWillExplode:)]) { [self.delegate widgetWillExplode:self]; }
Sending an @optional protocol method unconditionally assumes every conforming delegate implements it, which the @optional keyword explicitly denies. Any delegate that doesn't implement it walks straight into the full failure pipeline and, absent dynamic resolution or forwarding, crashes with unrecognized selector.
In the full message-dispatch failure pipeline, which stage runs immediately before doesNotRecognizeSelector: is called? A. Lookup B. Dynamic Resolution C. Full Forwarding D. Fast Forwarding
C. Full Forwarding — The pipeline runs Message -> Lookup -> Dynamic Resolution -> Fast Forwarding -> Full Forwarding -> doesNotRecognizeSelector: -> Exception. Full Forwarding (methodSignatureForSelector: + forwardInvocation:) is the last recovery stage attempted before the runtime gives up and calls doesNotRecognizeSelector:.
What does the default (NSObject) implementation of doesNotRecognizeSelector: actually do? A. It silently returns nil and lets the program continue B. It logs a warning to the console but does not crash C. It raises an NSInvalidArgumentException, which crashes the program if uncaught D. It automatically retries the message send after a short delay
C. It raises an NSInvalidArgumentException, which crashes the program if uncaught — doesNotRecognizeSelector:'s default behavior is to raise NSInvalidArgumentException. It is that raised exception, not the method call itself, that terminates the program when nothing catches it -- producing the classic 'unrecognized selector sent to instance' crash log.
Why must an override of doesNotRecognizeSelector: still call [super doesNotRecognizeSelector:aSelector] (or equivalent) after adding diagnostics? A. Because calling super is required by the Objective-C compiler for all overrides B. Because otherwise the exception is never raised and the original message send silently returns a garbage/undefined result instead of failing visibly C. Because super's implementation retries the original Lookup stage one more time D. It is purely a style convention with no functional effect
B. Because otherwise the exception is never raised and the original message send silently returns a garbage/undefined result instead of failing visibly — Diagnostics added in an override are meant to supplement the crash, not replace it. Skipping the call to super suppresses the exception, leaving the caller with an undefined result from a method that never actually ran -- a much harder failure to debug than a clear crash.
What is the correct role of -respondsToSelector: with respect to the failure pipeline? A. It is a stage inside the pipeline itself, run automatically on every message send B. It is a defensive check called by application code beforehand to avoid entering the failure pipeline at all C. It replaces doesNotRecognizeSelector: entirely on modern Apple platforms D. It can only be used inside forwardInvocation: overrides
B. It is a defensive check called by application code beforehand to avoid entering the failure pipeline at all — -respondsToSelector: is not part of the automatic dispatch-failure pipeline; it is a query application code calls proactively -- most idiomatically to guard @optional protocol methods -- to check whether a send would succeed, avoiding the entire pipeline for cases where it wouldn't.
Which stage, if it succeeds, causes the runtime to re-attempt the ORIGINAL method lookup rather than simply completing the message directly? A. Fast Forwarding (-forwardingTargetForSelector:) B. Full Forwarding (-forwardInvocation:) C. Dynamic Resolution (+resolveInstanceMethod: / +resolveClassMethod:) D. doesNotRecognizeSelector: itself
C. Dynamic Resolution (+resolveInstanceMethod: / +resolveClassMethod:) — Dynamic Resolution's job is to install a real method implementation on the fly (typically via class_addMethod) and return YES; on success, the runtime re-attempts the original Lookup stage, which now finds the newly-added method, exactly as if it had existed from the start.
What does the hexadecimal address in a crash message like '-[Widget explode]: unrecognized selector sent to instance 0x1007041a0' identify? A. The memory address of the Widget class's method table B. The address of the -explode selector itself C. The exact object instance that received the unhandled message D. The address of the crash reporter's log buffer
C. The exact object instance that received the unhandled message — The address identifies the specific receiving instance, not the class or the selector. It is often the fastest clue for distinguishing a genuinely missing method from a wrong-type object or a use-after-free where the address belongs to deallocated, reused memory.
A class Robot sends [self.controller navigate] where -navigate is an @optional method on a RobotControllerDelegate protocol. Write the defensive respondsToSelector: guard that should wrap this send. Then, separately, write a minimal doesNotRecognizeSelector: override for Robot that logs the selector name and the class name before calling super, and explain, in terms of the six-stage pipeline, exactly which stages would have to fail for that override to actually run.
if ([self.controller respondsToSelector:@selector(navigate)]) { [self.controller navigate]; }\n\nFor the override: - (void)doesNotRecognizeSelector:(SEL)aSelector { NSLog(@"%@ could not handle %@", NSStringFromClass([self class]), NSStringFromSelector(aSelector)); [super doesNotRecognizeSelector:aSelector]; }\n\nFor that override to run, Lookup must have found no method in Robot's class or its superclass chain, Dynamic Resolution's +resolveInstanceMethod: must have declined (or not been overridden), Fast Forwarding's -forwardingTargetForSelector: must have returned nil (or not been overridden), and Full Forwarding's -forwardInvocation: must have fallen through to its default NSObject implementation (or not been overridden) -- only then does the runtime finally call doesNotRecognizeSelector:.
respondsToSelector: takes a SEL, obtained via @selector(navigate). For the override, use NSStringFromSelector and NSStringFromClass, and remember super must still be called. For the pipeline explanation, walk through Lookup, Dynamic Resolution, Fast Forwarding, and Full Forwarding in order.