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
| Method | Use 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) insideIn 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 forgetDelegates
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
URLSessionthrow 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
URLProtocolsubclass to return fake responses. - Do I need Alamofire? Usually not. Modern
URLSessionwithasync/awaitcovers most needs.