Common Objective-C Errors: Compile-Time and Build Errors
Build a mental (and literal) reference library of the Objective-C errors that stop a build cold, so that when the compiler or linker throws one of these messages at you, you recognize it instantly, know why it happened, and know exactly how to fix it.
This lesson is organized as a searchable error library rather than a linear narrative. Each section below is named after the exact error text you will see in Xcode's issue navigator or in raw clang/ld build output, so you can scan the headings, find your error, and jump straight to the fix. Every entry follows the same six-step walk: what the error literally means, the most common real-world cause, a minimal broken example that reproduces it, the concrete fix, and how to prevent it from happening again. This first half covers compile-time and build-time errors — the ones that appear before your program ever runs, while clang is parsing and type-checking your source, or while the linker is stitching object files and libraries together into an executable. The companion lesson covers the second half: runtime crashes and exceptions that only appear once the program is actually executing.
expected identifier
Meaning: "expected identifier" is a syntax-level parse error. The compiler was reading your source code token by token, following the grammar of the language, and at some point it needed a name — a variable name, a type name, a parameter name — and instead found something else (a stray punctuation mark, a keyword, or nothing at all before a closing brace).
Common Cause: the overwhelming majority of these are caused by a missing semicolon on the line *before* the one the compiler actually flags, a stray or missing brace/parenthesis that shifts the parser's expectations, or a malformed declaration such as forgetting the type name in a method or variable declaration. Because the parser only realizes something is wrong once it hits the next token, the error is often reported one line later than the actual mistake.
Fix: work backwards from the flagged line. Look at the line immediately above it first — nine times out of ten a missing ; there is the real culprit. If the braces/parens look balanced and the previous line ends correctly, check the declaration itself for a missing type or a keyword typo.
Prevention: let Xcode's live syntax highlighting and bracket-matching do the work for you — a stray red squiggle or a bracket that doesn't highlight its partner is an early warning before you even hit Build. Keep declarations on their own lines while learning, and always terminate statements with ; as a reflex.
undeclared identifier / use of undeclared identifier
Meaning: the compiler encountered a name — a variable, function, class, or constant — that it has no record of. Objective-C (like C) requires every symbol to be declared (or its declaring header imported) before it is used, so referencing something the compiler has never seen produces "use of undeclared identifier 'X'".
Common Cause: the two most common causes are (1) a genuine typo in the identifier name that no longer matches the real declaration, and (2) forgetting to #import the header that declares the class, constant, or function you are trying to use — the symbol exists somewhere in your project, but the compiler processing *this* file has never been told about it.
Fix: first double-check spelling and capitalization (Objective-C is case-sensitive, so nsstring and NSString are different identifiers). If spelling is correct, add the missing #import "HeaderName.h" (or #import <Framework/Framework.h> for a system framework) at the top of the file so the declaration is visible before it's used.
Prevention: import headers as soon as you introduce a dependency on a new class, and lean on Xcode's autocomplete — if a symbol doesn't appear in the autocomplete list as you type it, that's a strong sign it isn't visible yet and a header is missing.
incompatible pointer types
Meaning: you assigned (or passed as an argument) a pointer of one Objective-C type where the compiler expected a pointer of a different, unrelated type. Unlike a plain warning about mismatched number types, this is a real type-safety catch — the compiler is telling you that the object on the right genuinely cannot be safely treated as the type on the left.
Common Cause: the classic case is assigning an NSString * where an NSArray * is expected (or vice versa) — often because a method was declared to return one type but you're capturing it into a variable of another, or because two similarly-named APIs return different collection types and you mixed them up.
Fix: trace the declared return type or parameter type of the method involved and change the receiving variable's type (or the value being passed) to match. If the mismatch is intentional and you genuinely need to bridge between two related types, use an explicit, correct cast rather than letting the compiler guess — but for unrelated types like NSString * and NSArray *, there is no valid cast; you have the wrong value.
Prevention: always declare local variables with the exact type documented for the API you're calling (checking the header or documentation rather than guessing), and pay attention to this warning/error immediately rather than dismissing it — it is one of the few compile-time guardrails Objective-C gives you against real logic bugs.
property not found
Meaning: you used dot syntax (or valueForKey:-style access in some tooling) to access a property that the compiler cannot find declared on the receiver's static type. The full message is usually something like "property 'X' not found on object of type 'Y'".
Common Cause: most often this is a simple typo in the property name, but it also happens frequently when the variable holding the object is typed as something too generic — most commonly id or a superclass — so the compiler has no @interface to search for that property, even though the underlying object really does have it at runtime. As covered in the earlier Properties lesson, @property declarations live on a specific interface, and the compiler only looks at the *declared* (static) type of your variable, not the actual runtime class.
Fix: check spelling first. If the property genuinely exists but the variable is too generically typed, either declare the variable with the correct, more specific class type, or cast the expression to that type at the point of access, e.g. ((MyClass *)obj).myProperty.
Prevention: avoid typing variables as id unless you specifically need dynamic/duck-typed behavior; prefer the most specific class type available so the compiler can verify every property and method access for you at compile time instead of failing at runtime.
no visible @interface declares selector
Meaning: you sent a message (called a method) that the compiler cannot find declared in any @interface, category, or protocol visible from the current file. As covered in the Interface and Implementation lesson, the compiler type-checks message sends against declared interfaces, not against the actual @implementation — if it can't see a declaration, it assumes the method doesn't exist.
Common Cause: the method really was only added to the @implementation and never declared in the corresponding @interface (or a private/class-extension interface), or it was declared in a category whose header simply isn't imported in this file, or — again — the object is statically typed too broadly for the compiler to know it has that method.
Fix: ensure the method is declared in an imported @interface, category, or protocol. If it's a private helper method, add it to a class-extension interface at the top of the .m file; if it belongs to another class, import that class's header.
Prevention: get in the habit of declaring every public method in the @interface the moment you write it in the @implementation, and keep private methods declared in a class extension rather than relying on the compiler's more lenient (and easy to misuse) implicit behaviors.
ARC forbids explicit message send of release
Meaning: this is a compile-time ARC guardrail, not a runtime problem. Under Automatic Reference Counting, as covered in the earlier ARC lesson, the compiler inserts every retain, release, and autorelease call for you automatically based on ownership rules — so it categorically refuses to let you write those calls yourself.
Common Cause: almost always this comes from old manual-reference-counting (MRC) code or tutorials being pasted into an ARC-enabled project, where the code still explicitly calls [obj release], [obj retain], [obj autorelease], or overrides -dealloc to call [super dealloc].
Fix: simply remove the explicit call. ARC handles memory management automatically the moment the object's ownership goes out of scope — there is nothing for you to manually release, retain, or autorelease under ARC.
Prevention: know which reference-counting model a file or target is using before pasting in code from an older source; when migrating legacy MRC code into a modern project, run Xcode's "Convert to Objective-C ARC" refactoring tool rather than manually deleting calls one by one.
duplicate symbol
Meaning: "duplicate symbol for architecture …" is a link-time error meaning the exact same symbol — most often a C function or a global variable — has a full definition in more than one compiled object file. The linker has two candidate definitions for the same name and refuses to pick one arbitrarily.
Common Cause: as covered in the early Compilation and Linking lesson, this typically happens when a function or global variable is defined (not just declared) directly in a header file that gets #imported into multiple .m files, so each translation unit compiles its own copy, and the linker sees the same symbol name defined twice when it combines all the object files.
Fix: if the symbol should be private to a single file, mark its definition static so it gets file-local (internal) linkage instead of external linkage — this is usually the right fix for helper functions defined in headers. If it's meant to be shared and genuinely was defined twice by accident (e.g. copy-pasted into two .m files), remove the duplicate definition and keep only one.
Prevention: put only *declarations* (extern variables, function prototypes) in headers, and put the single, real *definition* in exactly one .m file; when you do need a function's implementation inline in a header, mark it static inline.
undefined symbol
Meaning: the linker found a *reference* to a symbol somewhere in your compiled code — a call to a function, or a use of a class or global — but it could never find that symbol's actual definition anywhere in the object files or libraries it was given. This is the exact inverse of duplicate symbol: instead of two definitions, there are zero.
Common Cause: the most common cause by far is forgetting to link a required framework or library that provides the symbol (for example, referencing a class from a framework you never added to "Link Binary With Libraries"), followed by a source file that declares something in a header but whose implementation file was accidentally excluded from the build target's "Compile Sources" list.
Fix: identify which library or framework owns the missing symbol (the mangled/demangled symbol name in the error usually hints at the class or C function) and add it to the target's linked frameworks and libraries. If it's your own code, check the target membership checkbox on the .m file that implements it.
Prevention: whenever you start using a new system framework, immediately add it to the target's link phase before writing more code against it, and periodically check that every .m file that should belong to a target actually has that target's checkbox checked in the File Inspector.
linker errors
Meaning: "linker error" is the umbrella category covering every failure that happens at the link stage — after every file has compiled successfully — rather than during compilation itself. Duplicate symbol and undefined symbol (covered above) are the two most common specific linker errors, but the category also includes things like architecture mismatches and missing library search paths.
Common Cause: because linking happens after all individual files compile cleanly, a linker error almost always means every one of your .m files is syntactically and semantically valid on its own — the problem is in how the pieces are being assembled into a final executable, not in any single file's logic.
Fix: the practical way to distinguish a linker error from a compiler error when reading build output is to look at the wording and location: a compiler error names a specific file and line number and complains about syntax, types, or missing declarations, while a linker error mentions symbol names (often the mangled C++ / Objective-C runtime spellings), the word "Undefined symbols", "duplicate symbol", or "ld: symbol(s) not found", and does not point at a single line of your source.
Prevention: keep your project's target membership and "Link Binary With Libraries" phase tidy as the project grows, and whenever you see linker-flavored wording in the error list, immediately shift your investigation away from syntax and toward "what is or isn't linked/compiled into this target," rather than re-reading source code for typos.
selector not recognized (compile-time / static warning)
Meaning: this is a compile-time or static-analysis-level warning — distinct from the runtime crash "unrecognized selector sent to instance" covered in the companion runtime-errors lesson — telling you that the compiler could not find an implementation anywhere visible for a selector you are referencing, such as through @selector(...), performSelector:, or a delegate/protocol method check.
Common Cause: commonly seen when using @selector(methodName:) for a method that was renamed or never implemented, or when a protocol declares an optional method that no class visible to the compiler actually implements, making it impossible for the compiler to verify the selector resolves to anything real.
Fix: verify the selector name matches an actual implemented method exactly, including all colons for parameters (doThing:withValue: is a different selector than doThing:). If the method genuinely is implemented elsewhere but just isn't visible, import the header that declares it.
Prevention: understand the key distinction from its runtime cousin: this warning fires while clang is analyzing your source and can be caught and fixed before you ever run the app, whereas "unrecognized selector sent to instance" is an actual crash that only happens live, when a message send resolves to nothing at runtime — always prefer catching the selector problem here, at compile time, rather than discovering it as a crash later.
Code Examples
The missing semicolon after NSInteger count = 5 means the parser tries to read the next line as a continuation of the same statement, producing a confusing "expected identifier" error on the *following* line rather than the true offending line.
Common mistake: Always check the line above a syntax error first — the real cause is frequently one line earlier than reported.
CalculatorEngine is a real class elsewhere in the project, but this file never imported its header, so the compiler processing Calculator.m has no idea the type exists.
Common mistake: Add #import "CalculatorEngine.h" at the top of the file the moment you reference a new class.
@"Alice" produces an NSString *, not an NSArray *. The compiler correctly refuses the assignment because NSString and NSArray are unrelated types with no valid implicit conversion.
Common mistake: Wrap the value in an array literal @[ ... ] (or otherwise produce a real NSArray) so the types actually match.
emailAddress really is a property on User, but because user is declared as id, the compiler has no @interface to consult and cannot verify the property exists.
Common mistake: Type the variable as its real class (User *) instead of id so dot-syntax property access can be checked at compile time.
printDetailed is fully implemented in Report.m, but because it was never added to Report.h's @interface, no other file compiling against Report.h can see that it exists.
Common mistake: Declare every method meant to be called from outside the class in its @interface, not just its @implementation.
This code was written for manual reference counting, where you are responsible for balancing every retain with a release. Under ARC the compiler inserts these calls itself and refuses to let you write them by hand.
Common mistake: Delete the explicit release/dealloc calls — ARC manages the retain count automatically.
Because the function body lives directly in the header, every .m file that imports it compiles its own full definition, giving the linker two (or more) identically-named external symbols.
Common mistake: Mark header-defined helper functions static (or move the definition into a single .m file) to avoid external linkage collisions.
The header import satisfies the compiler (it knows the *declaration* of MKMapView), but the actual compiled implementation lives inside MapKit.framework, which was never added to the target's link phase, so the linker cannot find the symbol.
Common mistake: Importing a header is not the same as linking its framework — both steps are required.
Notice the message names object files (Checkout.o) and raw symbol names rather than a .m file and a line number — that shape ("Undefined symbols", "duplicate symbol", "ld: ...", "linker command failed") is the signature of a link-stage error, distinct from a compiler diagnostic.
Common mistake: When you see 'ld:' or 'linker command failed' in the output, stop looking for a syntax bug and start checking target membership and linked libraries instead.
"didFinishLodaing:" is a typo of the real selector "didFinishLoading:". Because no method anywhere in the visible codebase implements that exact spelling, the compiler flags it as an unrecognized selector reference — a static warning you can fix before ever running the app.
Common mistake: Selector names (including colons) must match an implemented method exactly; a typo here is caught at compile time, unlike the runtime crash version of this error.
Manually writing [obj release]; in a project built with ARC.
Remove the explicit call — ARC inserts retain/release/autorelease automatically based on ownership.
ARC forbids explicit message sends of release, retain, autorelease, and dealloc at compile time precisely because it manages the retain count for you; leftover MRC-style calls are a build error, not a runtime issue.
Defining a non-static function or global variable directly inside a shared header file.
Put only the declaration (extern) in the header, and the single real definition in one .m file, or mark the header definition static.
A header-defined symbol gets recompiled into every file that imports it, so the linker sees multiple definitions of the same external symbol and reports "duplicate symbol".
Importing a framework's header without also adding the framework to "Link Binary With Libraries".
Add the framework to the target's link phase as soon as you start using it, not just its header import.
Importing a header only satisfies the compiler's knowledge of declarations; the linker still needs the actual compiled framework binary to resolve the referenced symbols, or it reports "undefined symbol".
Declaring a variable as id purely out of habit, then using dot-syntax to access its properties.
Declare variables with the most specific real class type available so the compiler can verify property and method access.
With a variable typed id, the compiler has no @interface to check against, so a perfectly real property on the underlying object still produces a compile-time "property not found" error.
Implementing a helper method only in @implementation and never adding it to the @interface.
Declare every method (or at least private ones in a class extension) alongside writing its implementation.
The compiler type-checks message sends against visible interface declarations, not against what's actually implemented, so an undeclared-but-implemented method still triggers "no visible @interface declares selector".
You see 'expected identifier' on line 12, but line 12 looks syntactically fine on its own. What should you check first? A. Line 12's variable naming conventions B. Line 11, for a missing semicolon or unbalanced brace C. The file's import list D. The project's build settings
B. Line 11, for a missing semicolon or unbalanced brace — Because the parser only realizes something is wrong once it reads the next token, a missing semicolon or brace on the previous line very often surfaces as an 'expected identifier' error one line later.
What is the most direct fix for 'use of undeclared identifier' when the referenced class genuinely exists elsewhere in the project? A. Rename the class B. Add the framework to the linker C. Add an #import for the header that declares it D. Wrap the reference in a cast
C. Add an #import for the header that declares it — If the class is spelled correctly but simply hasn't been declared to this file, importing its header makes the declaration visible to the compiler.
Why does the compiler flag 'incompatible pointer types' when assigning an NSString * literal to an NSArray * variable? A. It's just a stylistic warning with no real meaning B. NSString and NSArray are unrelated types, and no valid implicit conversion exists between them C. NSArray variables can never hold literals D. The literal needs to be cast to (void *) first
B. NSString and NSArray are unrelated types, and no valid implicit conversion exists between them — This is a genuine type-safety catch: NSString and NSArray are unrelated classes, so assigning one where the other is expected is a real bug the compiler is right to reject.
A variable is declared id user, and user.emailAddress produces 'property not found', even though User really has that property. Why? A. emailAddress must be misspelled somewhere in User's interface B. id variables cannot use dot syntax under any circumstances C. The compiler only checks the variable's static (declared) type, and id gives it no interface to search D. The property must be declared 'atomic' to be found
C. The compiler only checks the variable's static (declared) type, and id gives it no interface to search — Property access is checked against the declared static type of the variable. Typing it as id removes the compiler's ability to look up the property, even though the real object supports it.
What is the fix for 'ARC forbids explicit message send of release'? A. Replace 'release' with 'autorelease' B. Wrap the call in an @autoreleasepool block C. Remove the explicit call entirely; ARC manages it automatically D. Add a manual retain to balance it first
C. Remove the explicit call entirely; ARC manages it automatically — Under ARC you never call release, retain, autorelease, or override dealloc's super call manually — the compiler inserts all of that itself, and explicit calls are a compile-time error.
A helper function's full body is written directly inside a shared header, and multiple .m files import that header. What link error results, and why? A. Undefined symbol, because headers can't contain function bodies B. Duplicate symbol, because each importing file compiles its own copy of the same externally-linked function C. Incompatible pointer types, because headers must only contain declarations D. No error — this is standard practice
B. Duplicate symbol, because each importing file compiles its own copy of the same externally-linked function — Each .m file that imports the header compiles its own definition of the function with external linkage, so the linker sees the same symbol name defined more than once.
You reference a class from a framework whose header you imported, but the linker reports 'Undefined symbols ... symbol(s) not found'. What's the fix? A. Rename the class to match the framework exactly B. Add the framework to the target's Link Binary With Libraries build phase C. Move the #import to the top of the file D. Mark the class as static
B. Add the framework to the target's Link Binary With Libraries build phase — Importing a header only gives the compiler the declaration; the linker still needs the compiled framework binary linked into the target to resolve the actual symbol.
How does a compile-time 'selector not recognized' warning differ from the runtime 'unrecognized selector sent to instance' crash? A. They are the same error with different names B. The compile-time version only applies to C functions, not Objective-C methods C. The compile-time warning can be caught and fixed before running the app; the runtime version is an actual crash that only happens live, during a message send D. The runtime version happens only in Swift-bridged code
C. The compile-time warning can be caught and fixed before running the app; the runtime version is an actual crash that only happens live, during a message send — 'selector not recognized' is a static warning from clang's analysis that a selector has no visible implementation; 'unrecognized selector sent to instance' is a live crash that occurs when an actual message send resolves to nothing at runtime, covered in the companion lesson.
- (void)configure {
NSInteger count = 5
NSLog(@"count is %ld", (long)count); // error: expected identifier or '('
}
// Fix: terminate the previous statement.
- (void)configure {
NSInteger count = 5;
NSLog(@"count is %ld", (long)count);
}// In Calculator.m — CalculatorEngine.h was never imported
@implementation Calculator
- (void)run {
CalculatorEngine *engine = [[CalculatorEngine alloc] init];
// error: use of undeclared identifier 'CalculatorEngine'
}
@end
// Fix: import the header that declares the class.
#import "CalculatorEngine.h"
@implementation Calculator
- (void)run {
CalculatorEngine *engine = [[CalculatorEngine alloc] init];
}
@end- (void)loadNames {
NSArray *names = @"Alice"; // error: incompatible pointer types
// 'names' is declared NSArray *, but an NSString * literal was assigned.
}
// Fix: match the declared type to the actual value.
- (void)loadNames {
NSArray *names = @[@"Alice"];
}id user = [self currentUser];
NSLog(@"%@", user.emailAddress);
// error: property 'emailAddress' not found on object of type 'id'
// Fix: declare the variable with its real, specific type.
User *user = [self currentUser];
NSLog(@"%@", user.emailAddress);// Report.h
@interface Report : NSObject
- (void)printSummary;
@end
// Report.m
@implementation Report
- (void)printSummary { /* ... */ }
- (void)printDetailed { /* implemented, but never declared in the header */ }
@end
// SomeOtherFile.m
[report printDetailed];
// error: no visible @interface for 'Report' declares the selector 'printDetailed'
// Fix: declare it in the header.
@interface Report : NSObject
- (void)printSummary;
- (void)printDetailed;
@end- (void)dealloc {
[_delegate release]; // error: ARC forbids explicit message send of 'release'
[super dealloc]; // error: ARC forbids explicit message send of 'dealloc'
}
// Fix: under ARC, remove the manual calls entirely.
// ARC releases ivars automatically; there is no need to override -dealloc
// at all unless you're cleaning up non-object resources.// Utils.h — defines (not just declares) a function
NSString *formattedDate(NSDate *date) {
return [NSDateFormatter localizedStringFromDate:date
dateStyle:NSDateFormatterShortStyle
timeStyle:NSDateFormatterNoStyle];
}
// Imported into both FileA.m and FileB.m —
// duplicate symbol '_formattedDate' in FileA.o and FileB.o
// Fix: mark it static so each file gets its own private copy.
static NSString *formattedDate(NSDate *date) { /* ... */ }#import <MapKit/MapKit.h>
MKMapView *mapView = [[MKMapView alloc] initWithFrame:self.view.bounds];
// Undefined symbols for architecture arm64:
// "_OBJC_CLASS_$_MKMapView", referenced from: ...
// ld: symbol(s) not found for architecture arm64
// Fix: add MapKit.framework to "Link Binary With Libraries" in Build Phases.// Typical linker-error output (note: no single source line is blamed):
// Undefined symbols for architecture x86_64:
// "_OBJC_CLASS_$_PaymentProcessor", referenced from:
// objc-class-ref in Checkout.o
// ld: symbol(s) not found for architecture x86_64
// clang: error: linker command failed with exit code 1 (use -v to see invocation)if ([delegate respondsToSelector:@selector(didFinishLodaing:)]) { // typo
[delegate didFinishLodaing:self];
}
// warning: undeclared selector 'didFinishLodaing:'
// Fix: correct the selector name to match the real method exactly,
// including every colon.
if ([delegate respondsToSelector:@selector(didFinishLoading:)]) {
[delegate didFinishLoading:self];
}