Common Objective-C Errors: Runtime Crashes and Exceptions

Extend the searchable error library to the errors that only appear once your Objective-C program is actually running — crashes, exceptions, and diagnostic warnings — so you can recognize each one by its signature message and resolve it with confidence.

Where the companion lesson covered errors the compiler and linker catch before your app ever launches, this lesson covers the other half of the library: problems that only surface once code is executing on a real (or simulated) device — a message sent to an object that no longer exists, an array accessed past its bounds, a background thread touching UIKit. Each entry below keeps the same searchable format: scan the heading for your exact crash or exception name, then read through Meaning, Common Cause, Fix, and Prevention. Several of these entries deliberately point back to earlier lessons — Memory Debugging, Key-Value Observing, Retain Cycles, Message Forwarding, NSArray, NSDictionary, Nullability, and Grand Central Dispatch — because runtime errors are rarely isolated topics; they are the practical, painful edge of concepts you've already studied, and this lesson is where those concepts come back to bite (or save) you.

unrecognized selector sent to instance

Meaning: this is a live crash (an NSInvalidArgumentException) that occurs when a message send is dispatched at runtime and the Objective-C runtime cannot find any method matching that selector on the receiving object, after exhausting every avenue to resolve it. This is the runtime counterpart to the compile-time 'selector not recognized' warning from the companion lesson — except here, there is no build-time safety net left; the app has already launched and this is happening live.

Common Cause: as covered in the earlier Message Forwarding lesson, the runtime doesn't crash immediately on a missed selector — it first tries dynamic method resolution and then message forwarding, and only raises this exception once every one of those mechanisms has also failed to produce a handler. In practice this usually means: a typo in a selector called via performSelector:, a delegate/data-source method that was renamed on one side but not the other, or — very commonly — the object is not actually the class you think it is (for example, a cell dequeued as the wrong subclass, or an object returned from a collection typed as id).

Fix: implement the missing method on the class that's actually receiving the message, correct a selector-name typo, or verify — with a breakpoint or NSLog(@"%@", [obj class]) — that the object really is the class you expect and not some other type that snuck in.

Prevention: avoid over-reliance on id-typed values and performSelector: with string-built selector names where a normal, statically-checked message send would do; whenever you truly rely on the runtime's forwarding machinery from the Message Forwarding lesson, make sure forwardingTargetForSelector: or methodSignatureForSelector:/forwardInvocation: actually covers every selector you intend to forward, or the missing case will surface as exactly this crash.

EXC_BAD_ACCESS

Meaning: EXC_BAD_ACCESS is a low-level Mach exception, not an Objective-C exception — it means the process tried to read or write memory that is not valid for it, at the hardware/OS level. Unlike an NSException, it typically cannot be caught in Objective-C code at all; it simply terminates the process.

Common Cause: by far the most common cause is a dangling pointer / use-after-free: an object was deallocated, but something still held (and then messaged) a raw pointer to the memory where it used to live, and that memory has since been reused for something else entirely, producing garbage or invalid access. It can also happen from stack overflows (deep or infinite recursion) or from manually mismanaged C-level memory (malloc/free misuse).

Fix: as covered in the earlier Memory Debugging lesson, the most reliable way to pinpoint the exact deallocation that caused a later EXC_BAD_ACCESS is to enable Zombie Objects (NSZombieEnabled) or run under Address Sanitizer, both of which turn the opaque crash into a precise, readable message naming the deallocated object and the message sent to it.

Prevention: enable Address Sanitizer routinely during development for any code doing manual memory tricks or bridging to C, and audit any place your code stores a raw (unretained) pointer or uses __unsafe_unretained — those are the spots where a dangling reference can survive past the object's real lifetime.

KVO observer crash

Meaning: this covers a family of crashes specific to Key-Value Observing — most commonly "An instance ... was deallocated while key value observers were still registered" or "Cannot remove an observer ... for the key path ... because it is not registered as an observer." As covered in the earlier Key-Value Observing lesson's crash-risk section, KVO's observer/observed relationship is manually managed and unforgiving of mismatches.

Common Cause: the deallocation crash happens when an observed object (or, more often, the *observer* itself) is deallocated while it is still registered via addObserver:forKeyPath:options:context: and nobody ever called the matching removeObserver:forKeyPath:. The 'not registered' crash happens the opposite way — calling removeObserver: for a key path that was never actually added, often from a double-removal or a removal in a code path that runs even when registration never happened.

Fix: ensure every addObserver:forKeyPath: call has exactly one matching removeObserver:forKeyPath: call, typically in -dealloc or -viewWillDisappear:/teardown code, guarded so it can't run twice or run without a matching add.

Prevention: track observation state explicitly with a boolean flag (or use the block-based/Combine-style KVO APIs where available) so you never attempt to remove an observer that wasn't added, and always pair add/remove calls symmetrically in the same class, ideally in setup/teardown methods that are guaranteed to run exactly once each.

retain cycle

Meaning: a retain cycle, covered in depth in the earlier dedicated Retain Cycles lesson, isn't a crash in the traditional sense — it's a leak. Two (or more) objects hold strong references to each other, so neither one's retain count can ever reach zero, and ARC never deallocates either of them, even after every other part of the app has let go.

Common Cause: the single most common source is a block captured as a property or ivar that references self strongly, while self also strongly holds that block (directly or indirectly) — for example, a completion-handler property set to a block that calls self.something without a weak capture. Delegate-style parent/child object graphs using strong on both sides are the other classic case.

Fix: break the cycle by making one side of the relationship weak instead of strong — typically the 'back-pointer' side (a delegate, a parent reference, or self inside a captured block via a __weak typeof(self) weakSelf = self; pattern).

Prevention: default delegate properties to weak, audit any block property or block-based API call where the block references self, and periodically profile with Instruments' Leaks/Allocations tool to catch retain cycles that manifest as ever-growing memory rather than an obvious crash.

message sent to deallocated instance

Meaning: this is the diagnostic-friendly cousin of EXC_BAD_ACCESS — a message was sent to an object whose memory has already been freed. Without special tooling this often just manifests as an opaque EXC_BAD_ACCESS crash with no useful information about which object or which message was involved.

Common Cause: the classic pattern is an over-release under manual reference counting, or — far more common in ARC code — an __unsafe_unretained or raw C pointer reference to an object that outlived its owner's actual lifetime, so by the time it's messaged, the memory has already been reclaimed.

Fix: as covered in the Memory Debugging lesson, turn on Zombie Objects, which intercepts messages to deallocated instances and, instead of corrupting memory silently, immediately raises a clear exception naming exactly which zombie object and which selector were involved — turning a mysterious opaque crash into a precise, actionable one.

Prevention: avoid __unsafe_unretained references unless there is a specific, well-understood reason to use one instead of __weak; __weak references automatically become nil when their object deallocates instead of dangling, which eliminates this entire class of crash outright.

nil unexpectedly used

Meaning: Objective-C generally tolerates sending a message to nil silently (it just returns nil/0/NO and does nothing), which surprises newcomers from other languages. But certain specific APIs explicitly do NOT tolerate nil and will crash or throw the moment they receive it, because the operation being requested is fundamentally meaningless with a nil value.

Common Cause: the most common triggers are inserting nil into a collection literal (@[a, nil, b] or @{key: nil}), an NSAssert/NSParameterAssert that explicitly checks for and rejects nil, or crossing a Swift-bridged API boundary that declares a parameter nonnull — Swift enforces that contract at the boundary and will trap if Objective-C hands it a nil value it promised never to send.

Fix: trace back to where the nil value originated (often an optional network response, a dictionary lookup that missed, or an uninitialized property) and either guard against it explicitly before it reaches the sensitive API, or supply a real fallback value.

Fix: as covered in the Nullability lesson, annotate your own APIs with nonnull/nullable so the compiler warns you at the call site *before* a nil value ever reaches a spot that will crash on it, turning a runtime surprise back into a compile-time warning.

Prevention: be deliberate about nullability annotations on every public method and property, and never insert an unchecked value straight from a network/JSON response directly into a collection literal without first confirming it isn't nil.

array out of bounds

Meaning: this crash — reported as NSRangeException, with a message like "index N beyond bounds for empty array" or "index N beyond bounds [0 .. count-1]" — happens when code accesses an NSArray at an index that does not exist in that array, whether because the array is smaller than expected or is empty entirely.

Common Cause: as covered in the earlier NSArray lesson, common culprits are off-by-one loop bounds (<= instead of < against count), assuming an array populated from an earlier network call or user input has a fixed minimum size when it may actually be empty, or indexing into the *wrong* array (one that's shorter than the one you meant).

Fix: add an explicit bounds check — if (index < array.count) — before every indexed access whose bounds aren't provably safe by construction, and prefer safe accessor patterns (like firstObject/lastObject, which return nil instead of crashing on an empty array) where applicable.

Prevention: treat any array whose length depends on external data (network responses, user-generated content, file parsing) as untrusted, and bounds-check before indexed access as a reflex rather than an afterthought.

dictionary nil key

Meaning: NSDictionary (and NSMutableDictionary) explicitly forbid nil as a key, and in most cases also forbid a bare nil as a value in literal/mutable-setter syntax — attempting either throws an exception at the moment of insertion, rather than silently storing a broken entry.

Common Cause: as covered in the earlier NSDictionary lesson, this typically happens when a key is computed dynamically (from user input, a model property, or a network response) and turns out to be nil in some edge case that wasn't tested, or when code tries to store an 'absent' value directly as nil instead of using the special NSNull placeholder object, which — as covered in Foundation Collections and Modern Syntax — exists specifically to represent 'nothing' inside collections that cannot hold real nil.

Fix: validate that any dynamically-computed key is non-nil before the dictionary insertion, and when you genuinely need to represent 'no value' for a given key, store [NSNull null] instead of nil and check for [NSNull null] when reading it back out.

Prevention: never trust a key or value sourced from parsed JSON or user input to be non-nil without checking first, and standardize on NSNull as your team's convention for 'present key, absent value' so nobody is tempted to pass a raw nil into dictionary syntax.

invalid argument

Meaning: NSInvalidArgumentException is the broader Foundation exception category that many of the crashes above actually belong to under the hood — array-out-of-bounds, dictionary-nil-key, and unrecognized-selector are all, technically, specific flavors of 'invalid argument.' As a standalone heading, it's worth recognizing as the general signature of "the API validated what you passed it, and rejected it outright rather than silently misbehaving."

Common Cause: beyond the specific cases already covered, this shows up broadly across Foundation whenever an API is handed a malformed format string, an out-of-range enum-like constant, a negative length/count where only non-negative values are valid, or an object of the wrong dynamic type passed into a method that validates its parameters strictly.

Fix: read the exception's reason string carefully in the console/crash log — Foundation's NSInvalidArgumentException messages are almost always specific about exactly which argument and which constraint was violated, which tells you precisely what to validate or correct at the call site.

Prevention: treat NSInvalidArgumentException as a signal that Foundation caught a real bug in your argument construction, not as a nuisance to work around — validating inputs before they reach any Foundation API call is the general prevention strategy tying together every crash in this category.

main-thread checker warnings

Meaning: the Main Thread Checker is a runtime diagnostic tool built into Xcode that specifically watches for UIKit/AppKit API calls made from a thread other than the main thread, and flags them the moment they happen with a clear warning (and, for some APIs, in later OS/Xcode versions, an outright crash) naming the exact API and thread involved.

Common Cause: as covered in the earlier Grand Central Dispatch lesson's main-thread-rules coverage, this happens when a background queue or thread — often the completion handler of a network request or a dispatch_async block on a background queue — directly updates a UI element (setting label.text, reloading a table view, adjusting a view's frame) without first dispatching that work back to the main queue.

Fix: wrap the UI-touching code in dispatch_async(dispatch_get_main_queue(), ^{ ... }) (or [NSOperationQueue mainQueue] addOperationWithBlock:]) so it actually executes on the main thread, keeping only the non-UI work (parsing, computation) on the background queue.

Prevention: treat 'UIKit calls must happen on the main thread' as an absolute rule reinforced by the GCD lesson, and get in the habit of dispatching back to the main queue as the very last step of any asynchronous completion handler that will touch the UI, rather than adding it reactively after seeing a warning.

Code Examples

uppercaseString only exists on NSString. When the loop reaches the NSNumber @42, the runtime exhausts dynamic resolution and forwarding (see the Message Forwarding lesson) and finally raises 'unrecognized selector sent to instance' because no NSNumber implements that method.

Common mistake: Check isKindOfClass: (or otherwise guarantee homogeneity) before sending a type-specific message to an id-typed value pulled from a mixed collection.

__unsafe_unretained does not keep the object alive and does not get nilled out on deallocation, so once temp goes out of scope and is freed, danglingRef points at invalid (or reused) memory.

Common mistake: Prefer __weak over __unsafe_unretained — a weak reference is automatically nilled when the object deallocates, eliminating the dangling-pointer crash entirely.

This class registers itself as a KVO observer but never unregisters before it (or the observed object) is deallocated, leaving a dangling observation that crashes when the runtime tries to clean it up.

Common mistake: Every addObserver:forKeyPath: needs a matching removeObserver:forKeyPath:, typically placed in -dealloc.

The block stored in completionHandler strongly captures self, and self strongly holds the block via the property — a classic two-node retain cycle that leaks both objects for the lifetime of the app.

Common mistake: Capture self weakly inside a block that the object itself owns, breaking the cycle.

An extra, incorrect manual release (here via an unsafe CFRelease bridge) drops the object's retain count to zero prematurely; any later message send touches freed memory.

Common mistake: Enable Zombie Objects (from the Memory Debugging lesson) to turn this opaque crash into a precise 'message sent to deallocated instance' report naming the object.

Array literal syntax @[ ... ] throws immediately if any element evaluates to nil — here, middleName was nil because the optional fetch found nothing, and that nil landed straight in the literal.

Common mistake: Check for nil before inserting a possibly-absent value into a collection literal, or build the collection conditionally.

scores.count is 3, with valid indices 0, 1, and 2. The <= comparison lets the loop run one iteration too many, indexing at 3, which is out of bounds.

Common mistake: Loop bounds against an array's count should use <, never <=, unless you have a very specific reason and an explicit bounds check.

Dictionary literal syntax rejects a nil value the same way array literals do. Since middleName is legitimately absent, it needs a real placeholder object rather than a bare nil.

Common mistake: Use [NSNull null] to represent an intentionally-absent value inside a dictionary, and check for it explicitly when reading the value back out.

NSMakeRange happily builds a range struct with a negative or out-of-bounds length, but substringWithRange: validates it strictly at call time and throws NSInvalidArgumentException rather than silently returning garbage.

Common mistake: Validate any computed NSRange (non-negative length, total within the source string's bounds) before passing it to a Foundation API.

NSURLSession completion handlers run on a background queue by default, but UILabel.text (like all UIKit properties) must only be touched from the main thread, exactly as covered in the GCD lesson's main-thread-rules section.

Common mistake: Wrap any UIKit/AppKit-touching code inside dispatch_async(dispatch_get_main_queue(), ^{ ... }) before it runs from a background completion handler.

Sending a type-specific message to an id-typed object pulled from a heterogeneous collection without checking its class.

Use isKindOfClass: to verify the object's real class before sending it a message that only some types support.

If the object turns out to be a class that doesn't implement that selector, the runtime exhausts dynamic resolution and forwarding and finally crashes with 'unrecognized selector sent to instance'.

Using __unsafe_unretained for a reference 'just to avoid retain cycles' without a specific reason.

Use __weak instead, which automatically becomes nil when the referenced object deallocates.

An __unsafe_unretained reference keeps pointing at freed memory after deallocation, producing EXC_BAD_ACCESS or 'message sent to deallocated instance' the next time it's used, whereas __weak safely nils itself out.

Registering a KVO observer with addObserver:forKeyPath: and never calling the matching removeObserver:forKeyPath:.

Pair every addObserver:forKeyPath: call with exactly one removeObserver:forKeyPath: call, typically in -dealloc.

An observer (or observed object) that deallocates while still registered crashes at teardown, as covered in the Key-Value Observing lesson's crash-risk section.

Inserting a possibly-nil value directly into an array or dictionary literal.

Check for nil first (or substitute [NSNull null] for a dictionary value that's intentionally absent) before building the literal.

Collection literal syntax throws immediately on encountering a nil element or value, even though ordinary Objective-C message sends tolerate nil silently.

Updating a UILabel, UITableView, or other UIKit control from inside a network completion handler or background dispatch queue.

Wrap any UI-touching code in dispatch_async(dispatch_get_main_queue(), ^{ ... }) before it runs.

UIKit/AppKit APIs must only be touched from the main thread; the Main Thread Checker exists specifically to flag this violation, as covered in the GCD lesson's main-thread-rules section.

What has to happen before the runtime finally raises 'unrecognized selector sent to instance'? A. Nothing — it crashes the instant a matching method isn't found B. The runtime first attempts dynamic method resolution and message forwarding, and only crashes once both have failed C. It only ever happens with performSelector:, never with normal message sends D. It waits for the next KVO notification cycle

B. The runtime first attempts dynamic method resolution and message forwarding, and only crashes once both have failed — As covered in the Message Forwarding lesson, the runtime tries dynamic resolution and forwarding before giving up and raising the exception — this crash is really the final fallback of that whole mechanism.

Why is EXC_BAD_ACCESS usually harder to diagnose than a typical NSException-based crash? A. It only happens in Swift code B. It's a low-level Mach exception, not an Objective-C exception, so it usually can't be caught and doesn't name the object or message involved by default C. It always happens on a background thread D. It's actually the same thing as unrecognized selector

B. It's a low-level Mach exception, not an Objective-C exception, so it usually can't be caught and doesn't name the object or message involved by default — EXC_BAD_ACCESS operates below the Objective-C exception layer; enabling Zombie Objects or Address Sanitizer (from the Memory Debugging lesson) is what turns it into a readable, specific crash report.

A class registers itself with addObserver:forKeyPath: but never calls the matching removeObserver:forKeyPath:. What's the likely eventual crash? A. EXC_BAD_ACCESS immediately upon registration B. 'An instance ... was deallocated while key value observers were still registered' C. Array out of bounds D. Retain cycle warning at compile time

B. 'An instance ... was deallocated while key value observers were still registered' — KVO's add/remove calls must be symmetrically paired; deallocating while still registered as an observer produces exactly this crash, covered in the KVO lesson's crash-risk section.

What's the standard fix for a retain cycle caused by a block property that captures self strongly? A. Delete the property entirely B. Capture self weakly inside the block (e.g. __weak typeof(self) weakSelf = self;) C. Call [self release] manually inside the block D. Mark the property 'unsafe_unretained' instead of 'copy'

B. Capture self weakly inside the block (e.g. __weak typeof(self) weakSelf = self;) — A weak capture of self inside the block breaks the strong-strong cycle between the object and the block it owns, letting both deallocate normally once nothing else references them.

What Xcode/debugging technique turns an opaque 'message sent to deallocated instance' crash into a precise, readable one naming the object and selector? A. Address Sanitizer disabled, Zombie Objects disabled B. Enabling Zombie Objects (NSZombieEnabled) C. Turning on the Main Thread Checker D. Switching the project from ARC to MRC

B. Enabling Zombie Objects (NSZombieEnabled) — Zombie Objects, covered in the Memory Debugging lesson, intercepts messages sent to deallocated instances and reports exactly which zombie object and selector were involved instead of corrupting memory silently.

Why does @[firstName, middleName, lastName] crash when middleName is nil, even though Objective-C normally tolerates sending messages to nil? A. Array literals are a special exception that explicitly reject nil elements and throw immediately B. NSString objects can never be nil C. The crash is actually unrelated to nil at all D. @[] syntax silently drops nil elements instead of crashing

A. Array literals are a special exception that explicitly reject nil elements and throw immediately — While ordinary message sends to nil are silently tolerated, collection literal syntax specifically forbids nil elements/values and throws immediately upon encountering one, as covered in the Nullability lesson.

A loop uses for (NSInteger i = 0; i <= array.count; i++) to iterate an NSArray. What's wrong, and what crash results? A. Nothing is wrong; <= is the correct comparison for array bounds B. The <= lets the loop index one past the last valid element, causing an NSRangeException ('index beyond bounds') C. It causes EXC_BAD_ACCESS instead of a range exception D. It only fails on empty arrays

B. The <= lets the loop index one past the last valid element, causing an NSRangeException ('index beyond bounds') — Valid indices run from 0 to count-1; using <= against count allows one extra iteration at index == count, which is out of bounds and throws NSRangeException.

What does the Main Thread Checker warn about, and what's the correct fix? A. It warns about retain cycles; fix by using weak references B. It warns when UIKit/AppKit API calls happen off the main thread; fix by dispatching the UI-touching code back to dispatch_get_main_queue() C. It warns about missing KVO observer removal; fix by adding removeObserver: D. It warns about nil values in dictionaries; fix by using NSNull

B. It warns when UIKit/AppKit API calls happen off the main thread; fix by dispatching the UI-touching code back to dispatch_get_main_queue() — As covered in the GCD lesson's main-thread-rules section, UIKit/AppKit must only be touched from the main thread; the fix is wrapping the UI update in dispatch_async(dispatch_get_main_queue(), ^{ ... }).

NSArray *mixedItems = @[@"Alice", @42];
for (id item in mixedItems) {
    NSLog(@"%@", [item uppercaseString]);
}
// crash on the NSNumber: -[__NSCFNumber uppercaseString]:
// unrecognized selector sent to instance 0x...

// Fix: check the actual class before sending a type-specific message.
for (id item in mixedItems) {
    if ([item isKindOfClass:[NSString class]]) {
        NSLog(@"%@", [item uppercaseString]);
    }
}
__unsafe_unretained NSObject *danglingRef;
{
    NSObject *temp = [[NSObject alloc] init];
    danglingRef = temp; // unsafe: no strong ownership
} // temp deallocates here; memory may already be reused

NSLog(@"%@", danglingRef); // EXC_BAD_ACCESS (or worse: silent corruption)

// Fix: use __weak instead, which becomes nil automatically on dealloc.
__weak NSObject *safeRef;
{
    NSObject *temp = [[NSObject alloc] init];
    safeRef = temp;
}
NSLog(@"%@", safeRef); // nil — logs harmlessly instead of crashing
- (void)startObserving {
    [self.model addObserver:self
                  forKeyPath:@"status"
                     options:0
                     context:nil];
}
// no matching removeObserver:forKeyPath: anywhere in this class
// crash later: 'An instance ... was deallocated while key value
// observers were still registered'

// Fix: always pair add with a guaranteed remove.
- (void)dealloc {
    [self.model removeObserver:self forKeyPath:@"status"];
}
@property (nonatomic, copy) void (^completionHandler)(void);

- (void)configure {
    self.completionHandler = ^{
        [self refreshUI]; // strongly captures self
    };
    // self -> completionHandler -> block -> self : cycle, never deallocates
}

// Fix: capture a weak reference instead.
- (void)configure {
    __weak typeof(self) weakSelf = self;
    self.completionHandler = ^{
        [weakSelf refreshUI];
    };
}
// Legacy MRC-style bug ported incorrectly, or a raw pointer outliving its owner
Logger *logger = [[Logger alloc] init];
CFRelease((__bridge CFTypeRef)logger); // manually over-releases via bridging
[logger log:@"still alive?"]; // crash: message sent to deallocated instance

// Fix: enable Zombie Objects during debugging to get a precise crash
// message naming the exact deallocated object and selector involved,
// then remove the incorrect manual release/bridging call entirely.
NSString *middleName = [self fetchOptionalMiddleName]; // may return nil
NSArray *nameParts = @[firstName, middleName, lastName];
// crash: '*** -[__NSPlaceholderArray initWithObjects:count:]:
// attempt to insert nil object from objects[1]'

// Fix: guard against nil before building the literal.
NSMutableArray *nameParts = [NSMutableArray arrayWithObjects:firstName, lastName, nil];
if (middleName) {
    [nameParts insertObject:middleName atIndex:1];
}
NSArray *scores = @[@90, @85, @77];
for (NSInteger i = 0; i <= scores.count; i++) { // should be <
    NSLog(@"%@", scores[i]);
}
// crash on i == 3: '*** -[__NSArrayI objectAtIndex:]:
// index 3 beyond bounds [0 .. 2]'

// Fix: use a strict less-than comparison against count.
for (NSInteger i = 0; i < scores.count; i++) {
    NSLog(@"%@", scores[i]);
}
NSString *middleName = nil; // user has no middle name
NSDictionary *profile = @{@"first": @"Sam", @"middle": middleName, @"last": @"Lee"};
// crash: attempt to insert nil object from objects[1]

// Fix: represent 'no value' with NSNull, not nil.
NSDictionary *profile = @{
    @"first": @"Sam",
    @"middle": middleName ?: [NSNull null],
    @"last": @"Lee"
};
NSInteger start = 5;
NSInteger length = someComputedValue - start; // can be negative
NSString *substring = [fullString substringWithRange:NSMakeRange(start, length)];
// *** -[__NSCFString substringWithRange:]: Range or index out of bounds
// (an NSInvalidArgumentException)

// Fix: validate the computed range before use.
if (length >= 0 && start + length <= fullString.length) {
    NSString *substring = [fullString substringWithRange:NSMakeRange(start, length)];
}
[NSURLSession.sharedSession dataTaskWithURL:url
                           completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    // this completion handler runs on a background queue
    self.statusLabel.text = @"Loaded"; // Main Thread Checker warning
}] resume];

// Fix: dispatch UI updates back to the main queue.
[NSURLSession.sharedSession dataTaskWithURL:url
                           completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    dispatch_async(dispatch_get_main_queue(), ^{
        self.statusLabel.text = @"Loaded";
    });
}] resume];