iosNudge

How do you make network requests with URLSession?

EasyAsked oftenNetworkingConcurrency 2 min read

TL;DR

Use URLSession with async/await: let (data, response) = try await session.data(for: request). Always check the HTTP status code. URLSession does not throw for 404 or 500. Pick a URLSessionConfiguration for timeouts, caching, headers and background downloads.

A simple GET request

struct User: Decodable { let id: Int; let name: String }

enum APIError: Error {
    case badStatus(Int)
}

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let http = response as? HTTPURLResponse else { throw URLError(.badServerResponse) }
    guard (200..<300).contains(http.statusCode) else { throw APIError.badStatus(http.statusCode) }

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

A POST request with a body

var request = URLRequest(url: URL(string: "https://api.example.com/posts")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.httpBody = try JSONEncoder().encode(NewPost(title: "Hello"))

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

Task types

MethodUse for
data(for:)Normal API calls, small responses
download(for:)Large files, saved to a temp file on disk
upload(for:from:)Sending data or a file
bytes(for:)Streaming the response line by line
webSocketTask(with:)WebSockets

Configurations

let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 15
config.waitsForConnectivity = true
config.httpAdditionalHeaders = ["Accept": "application/json"]
let session = URLSession(configuration: config)
  • .default: disk cache, cookies, credentials.
  • .ephemeral: nothing saved to disk. Good for private browsing or sensitive data.
  • .background(withIdentifier:): the system does the transfer, even if your app is suspended or killed.

Create a session once and reuse it. Each session has its own connection pool.

Cancellation

With async/await, cancelling the Swift Task cancels the request.

let task = Task { try await fetchUser(id: 1) }
task.cancel() // throws URLError(.cancelled) inside

In SwiftUI, .task { } cancels the request when the view disappears.

The old way (still in many codebases)

let task = URLSession.shared.dataTask(with: url) { data, response, error in
    // Runs on a background queue. Hop to main before touching UI.
    DispatchQueue.main.async { /* update UI */ }
}
task.resume() // easy to forget

Delegates

Use a URLSessionDelegate (or task delegate) for:

  • Authentication challenges and certificate pinning.
  • Progress for uploads and downloads.
  • Redirect control.
  • Background session events.

Handling errors well

do {
    let user = try await fetchUser(id: 1)
} catch let error as URLError where error.code == .notConnectedToInternet {
    showOfflineBanner()
} catch is DecodingError {
    log("Server sent unexpected JSON")
} catch {
    showGenericError()
}

Common follow-up questions

  • Does URLSession throw on HTTP 500? No. It only throws on transport errors. You check the status code.
  • Which thread does data(for:) resume on? It resumes in the caller's isolation. If the caller is @MainActor, you are back on the main actor.
  • How do you add retry? Wrap the call in a loop with a backoff Task.sleep, and retry only for safe cases like timeouts or 503.
  • How do you test network code? Inject a protocol, or use a custom URLProtocol subclass to return fake responses.
  • Do I need Alamofire? Usually not. Modern URLSession with async/await covers most needs.