PromiseKit
Objective-C
promises
asynchronous programming
iOS development

Objc PromiseKit Add new promises from within a promise

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Adding a new async operation inside a PromiseKit chain is standard practice in Objective-C, but it must be done with the correct return pattern. If a then block starts a new request and does not return it, sequencing and error propagation break. Clean promise composition keeps async flows predictable, testable, and easier to refactor.

Return the New Promise From then

Whenever the next step is asynchronous, return the promise from the then block.

objective-c
1@import PromiseKit;
2
3- (AnyPromise *)loadUserAndOrders {
4    return [self fetchUser].then(^AnyPromise *(NSDictionary *user) {
5        NSNumber *userId = user[@"id"];
6        return [self fetchOrdersForUser:userId];
7    }).then(^id(NSArray *orders) {
8        NSLog(@"orders count: %lu", (unsigned long)orders.count);
9        return orders;
10    });
11}

PromiseKit flattens the returned promise, so downstream steps wait for completion automatically.

Anti-Pattern to Avoid

Incorrect pattern:

  • start nested async call inside then
  • do not return the nested promise
  • outer chain continues early

That leads to race conditions and hidden failures. If you ever see nested callbacks inside then without a return statement, fix it immediately.

Conditional Async Branching

You can return different promises depending on the previous result.

objective-c
1- (AnyPromise *)loadProfileWithRefreshIfNeeded {
2    return [self readCachedProfile].then(^AnyPromise *(NSDictionary *profile) {
3        BOOL stale = [profile[@"stale"] boolValue];
4        if (stale) {
5            return [self fetchRemoteProfile];
6        }
7        return profile;
8    }).then(^id(NSDictionary *finalProfile) {
9        return [self persistProfile:finalProfile];
10    });
11}

All branches should return compatible value types for the next chain step.

Fan-Out and Join With PMKWhen

After one async step, you may need parallel calls and one join point.

objective-c
1- (AnyPromise *)loadDashboard {
2    return [self fetchUser].then(^AnyPromise *(NSDictionary *user) {
3        NSNumber *uid = user[@"id"];
4        AnyPromise *orders = [self fetchOrdersForUser:uid];
5        AnyPromise *messages = [self fetchMessagesForUser:uid];
6        return PMKWhen(@[orders, messages]);
7    }).then(^id(NSArray *results) {
8        return @{
9            @"orders": results[0],
10            @"messages": results[1]
11        };
12    });
13}

This pattern removes manual counter logic and keeps failure behavior consistent.

Error Handling and Recovery

Use catch for terminal error handling. Use recover where a fallback is valid.

objective-c
1[[self loadUserAndOrders] recover:^id(NSError *error) {
2    NSLog(@"recovering: %@", error.localizedDescription);
3    return @[];
4}].then(^id(NSArray *orders) {
5    NSLog(@"continuing with %lu orders", (unsigned long)orders.count);
6    return nil;
7}).catch(^(NSError *error) {
8    NSLog(@"unhandled failure: %@", error.localizedDescription);
9});

Do not hide errors with broad recovery unless the business case is explicit.

Lifetime and Capture Management

Long async chains can keep objects alive unintentionally. For UI controllers, weak capture can prevent retain cycles.

objective-c
1__weak typeof(self) weakSelf = self;
2return [self fetchUser].then(^AnyPromise *(NSDictionary *user) {
3    __strong typeof(weakSelf) strongSelf = weakSelf;
4    if (!strongSelf) {
5        return [NSError errorWithDomain:@"App" code:1001 userInfo:nil];
6    }
7    return [strongSelf fetchOrdersForUser:user[@"id"]];
8});

Use this carefully, since deallocated owners might mean updates should be skipped intentionally.

Test Promise Composition

Unit tests should verify:

  • order of async steps
  • branch behavior for stale and fresh cache paths
  • rejection propagation from nested promises
  • fallback behavior from recover

Mock each dependency as a promise-producing method so you can test flow logic deterministically. It also helps to log a lightweight chain identifier through each step so production traces show where a promise path diverged or failed. For team code reviews, require every then block to show whether it returns a plain value or a promise, since that single distinction explains most sequencing defects.

Common Pitfalls

  • Launching nested async work inside then and forgetting to return the new promise.
  • Mixing callback-style APIs and promises without adaptation boundaries.
  • Returning inconsistent types from conditional branches in one chain.
  • Swallowing critical errors with over-broad recover logic.
  • Capturing controller instances strongly in long-lived chains.

Summary

  • Promise chaining in Objective-C is reliable when every async step returns a promise.
  • Conditional and parallel workflows are clean with returned promises and PMKWhen.
  • Error handling should distinguish recovery paths from terminal failures.
  • Capture strategy matters for UI lifetime and memory safety.
  • Focus tests on sequence, branch correctness, and rejection propagation.

Course illustration
Course illustration

All Rights Reserved.