Section 1/202 menit

1. async/await

1. async/await

Teori: Masalah Callback Hell dan Cooperative Multitasking

Sebelum async/await, concurrency di Swift (dan platform Apple) dibangun di atas completion handlers — closure yang dipanggil ketika operasi async selesai. Ini menciptakan beberapa masalah fundamental:

1. Callback Hell — Nesting yang Tidak Terkendali

swift
// Completion handler: setiap level async = satu level nesting
func loadUserProfile(userID: String) {
    fetchUser(userID: userID) { user in
        guard let user = user else { return }
        fetchAvatar(url: user.avatarURL) { image in
            guard let image = image else { return }
            fetchPosts(userID: user.id) { posts in
                guard let posts = posts else { return }
                // Logika sesungguhnya berada di kedalaman 3 closure
                // Error handling di setiap level
                // Sulit dibaca, sulit di-maintain
                DispatchQueue.main.async {
                    self.display(user: user, image: image, posts: posts)
                }
            }
        }
    }
}

2. Error Handling yang Tersebar

swift
// Setiap callback harus handle error sendiri — tidak ada propagation
fetchUser(id: id) { result in
    switch result {
    case .failure(let error):
        // handle error 1
        return
    case .success(let user):
        fetchAvatar(url: user.avatarURL) { result in
            switch result {
            case .failure(let error):
                // handle error 2 — duplikasi
                return
            case .success(let image):
                // ... terus
            }
        }
    }
}

3. Kehilangan Thread Context

swift
// Callback bisa dipanggil di thread mana saja
// Tidak ada jaminan thread-safety
fetchData { result in
    // Thread apa ini? Main? Background? URLSession queue?
    self.updateUI(result)  // UNSAFE jika bukan main thread
}

Mengapa async/await Lebih Baik

async/await bukan hanya syntactic sugar. Di baliknya, Swift menggunakan cooperative multitasking — model yang berbeda fundamental dari thread-based concurrency:

swift
Thread-based (GCD):          Cooperative (async/await):
─────────────────────        ─────────────────────────────
Thread 1: Task A runs        Thread 1: Task A runs
Thread 1: Task A blocks      Task A hits 'await'SUSPENDS
  (thread idle, wasted)      Thread 1: picks up Task B
Thread 2: needed for         Task A resumes when ready
  continuation               Thread 1: Task A continues
  (context switch overhead)  (tidak ada wasted threads)

Ketika kamu menulis await, fungsi di-suspend — bukan di-block. Thread yang digunakan dibebaskan untuk mengerjakan task lain. Ini memungkinkan ribuan concurrent operations dengan hanya beberapa thread (sesuai jumlah CPU core).

Sintaks dan Cara Kerja

swift
// Setelah: linear, mudah dibaca
func loadUserProfile(userID: String) async throws -> UserProfile {
    let user = try await fetchUser(userID: userID)          // suspend 1
    let image = try await fetchAvatar(url: user.avatarURL)  // suspend 2
    let posts = try await fetchPosts(userID: user.id)       // suspend 3
    return UserProfile(user: user, image: image, posts: posts)
}

// Error propagates dengan throws — tidak perlu handle di setiap level
// Thread safety dijamin oleh Swift runtime (kembali ke caller's executor)

Suspension Point — Apa yang Terjadi di Balik await:

swift
1. Kode sebelum await berjalan
2. await: fungsi di-suspend, eksekusi dikembalikan ke Swift runtime
3. Runtime: pilih task lain yang siap berjalan
4. Ketika I/O selesai: runtime jadwalkan ulang suspended function
5. Kode setelah await berjalan (mungkin di thread berbeda, tapi isolation terjaga)

Memanggil async dari Synchronous Context

swift
// async function tidak bisa dipanggil langsung dari sync — harus dalam Task
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Task {} adalah "jembatan" dari sync ke async
        Task {
            do {
                let profile = try await loadUserProfile(userID: "123")
                await MainActor.run { updateUI(with: profile) }
            } catch {
                await MainActor.run { showError(error) }
            }
        }
    }
}

Kapan Menggunakan async/await

Gunakan ketika:

  • Operasi yang melibatkan I/O: network, disk, database
  • Operasi yang membutuhkan waktu: compute-intensive background tasks
  • Rangkaian operasi yang saling bergantung (sequential async)
  • Mengganti completion handler yang sudah ada

Jangan gunakan ketika:

  • Operasi synchronous yang cepat (< 1ms) — overhead task tidak sebanding
  • Hot path / tight loop numerik — gunakan synchronous code biasa
  • Kode yang harus berjalan di context tanpa Swift runtime (misalnya C interop layer)

Real Use Case: Network Request Chain

swift
// Layanan authentication dengan token refresh otomatis
actor AuthenticatedAPIClient {
    private var accessToken: String?
    private var refreshToken: String?
    private let baseURL: URL

    init(baseURL: URL) { self.baseURL = baseURL }

    func authenticatedRequest<T: Decodable>(
        endpoint: String,
        type: T.Type
    ) async throws -> T {
        // 1. Pastikan punya token valid
        let token = try await ensureValidToken()

        // 2. Buat request
        var request = URLRequest(url: baseURL.appendingPathComponent(endpoint))
        request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

        // 3. Kirim request
        let (data, response) = try await URLSession.shared.data(for: request)

        // 4. Handle 401 — refresh token dan coba lagi
        if let http = response as? HTTPURLResponse, http.statusCode == 401 {
            try await refreshAccessToken()
            return try await authenticatedRequest(endpoint: endpoint, type: type)
        }

        return try JSONDecoder().decode(T.self, from: data)
    }

    private func ensureValidToken() async throws -> String {
        if let token = accessToken { return token }
        try await refreshAccessToken()
        return accessToken!
    }

    private func refreshAccessToken() async throws {
        guard let refresh = refreshToken else { throw AuthError.notLoggedIn }
        var request = URLRequest(url: baseURL.appendingPathComponent("/auth/refresh"))
        request.httpMethod = "POST"
        request.httpBody = try JSONEncoder().encode(["refresh_token": refresh])
        let (data, _) = try await URLSession.shared.data(for: request)
        let tokens = try JSONDecoder().decode(TokenResponse.self, from: data)
        accessToken = tokens.accessToken
        refreshToken = tokens.refreshToken
    }
}

struct TokenResponse: Decodable { let accessToken, refreshToken: String }
enum AuthError: Error { case notLoggedIn }