Introduction

When I started shipping production iOS apps that call several REST endpoints, I often ended up with arrays of results that needed to be merged into a single feed. Each endpoint returned a Result<[Item], Error>, and I had to combine them without losing the error context. The pattern I settled on leverages Swift’s Result type together with flatMap, giving me a concise, type‑safe way to flatten nested arrays while preserving failure information.

The Problem with Nested Arrays

Imagine a news app that fetches three different sources: headlines, trending articles, and local reports. Each source is represented by a service method like this:

func fetchHeadlines() async throws -> [Headline]
func fetchTrending() async throws -> [TrendingArticle]
func fetchLocal() async throws -> [LocalReport]

After calling them, you end up with three separate arrays. You also need to present a unified list to the user, but you must surface any network error that occurred during the fetch. The naive approach is to collect the three arrays into a tuple and then manually pattern‑match each Result to decide whether to combine or propagate the error. That quickly becomes verbose and hard to read.

A Clean Solution: Using flatMap on Result

Swift’s Result provides flatMap that works just like its Optionals counterpart but also respects error propagation. By mapping each source to a Result<[Item], Error> and then flattening them, we get a single Result<[Item], Error] representing the combined feed.

Below is a production‑ready extension that does exactly that. It also includes a helper to keep the code DRY.

import Foundation

/// A generic wrapper that holds either a value or an error.
public typealias NetworkResult = Result

extension NetworkResult where Success: Sequence {
    /// Flattens an array of results that each contain a Sequence into a single result containing a concatenated sequence.
    /// - Parameter results: An array of NetworkResult where each Success is a Sequence (e.g., [Item]).
    /// - Returns: A single NetworkResult whose success value is the concatenated elements of all successful sequences, or the first encountered error.
    static func flatten(_ results: [NetworkResult]) -> NetworkResult<[Success.Element]> {
        results.reduce(into: .success([])) { partialResult, next in
            switch (partialResult, next) {
            case let (.success(acc), .success(nextSeq)):
                // Concatenate sequences safely.
                partialResult = .success(acc + nextSeq)
            case let (.failure(error), .success(_)):
                // Preserve the first error.
                partialResult = .failure(error)
            case let (.success(_), .failure(error)):
                // Replace success with the encountered error.
                partialResult = .failure(error)
            case let (.failure(firstError), .failure(secondError)):
                // Keep the first error; second is ignored.
                partialResult = .failure(firstError)
            }
        }
    }
}

// Usage example:
func fetchHeadlines() async -> NetworkResult<[Headline]> { /* implementation */ }
func fetchTrending() async -> NetworkResult<[TrendingArticle]> { /* implementation */ }
func fetchLocal() async -> NetworkResult<[LocalReport]> { /* implementation */ }

func loadFeed() async -> NetworkResult<[AnyItem]> {
    let results: [NetworkResult] = [
        await fetchHeadlines(),
        await fetchTrending(),
        await fetchLocal()
    ]
    // Because each result’s Success type is a Sequence, we can safely call flatten.
    return NetworkResult.flatten(results)
}

The key insight is that flatMap on Result automatically unwraps the inner value if it’s also a Result, discarding the wrapper while preserving the error chain. By providing a custom flatten we keep the logic readable and reusable across different services.

Why This Works: Error Safety and Readability

Using flatMap (or our extension) ensures two things:

  • Error propagation – As soon as any source fails, the whole operation fails with that error. No need to manually inspect each result.
  • Type safety – The compiler guarantees that we never mix unrelated types. If a service returns [Headline] and another returns [Int], the type system will stop us from flattening them together.

Moreover, the pattern abstracts away the boilerplate of pattern‑matching arrays of results. You can drop new sources into the array without touching the flattening logic, which reduces regression risk in production.

Pro tip: When you have a growing number of async APIs that return sequences, consider creating a protocol like SequenceProvider that defines a func results() -> [NetworkResult. This makes the flattening step even cleaner and paves the way for future refactoring.

Extending the Pattern: Combining Multiple Sources

Sometimes you need to merge results from multiple endpoints that return different but related types. You can use the same flattening technique by lifting each result into a common generic container. For example, if you have [Headline] and [Image] that both conform to a FeedItem protocol, you can map them to [any FeedItem] before flattening.

protocol FeedItem {}

extension NetworkResult where Success: Sequence, Success.Element: FeedItem {
    static func merge(_ results: [NetworkResult]) -> NetworkResult<[T]> {
        // Filter out non‑T elements after concatenation.
        return results.flatten().map { $0.compactMap { $0 as? T } }
    }
}

// Example usage:
let merged = await merge([await fetchHeadlines(), await fetchImages()])

This extension shows how the core flattening idea can be adapted for more complex scenarios without sacrificing clarity.

Conclusion

Flattening nested arrays of network results is a common pain point, but Swift’s Result type gives us a built‑in tool for handling errors elegantly. By leveraging flatMap (or a custom flatten extension), we can combine multiple async fetches into a single, type‑safe operation that either returns a unified feed or propagates the first error encountered. The pattern scales well as you add new data sources and keeps your code readable for teammates who might later maintain the feature.

Give it a try on your next project where you’re stitching together several API calls. You’ll find the implementation both concise and robust, letting you focus on business logic rather than error‑handling boilerplate.