Networking with URLSession & async/await
Reviewed & published by Brayan K
Fetch real data from the web. You'll call URLSession.shared.data(from:) with async/await, decode JSON into Swift types with Codable, check HTTP status codes, and handle errors cleanly.
Part of the free Swift course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- • Fetch data with URLSession.shared.data(from:)
- • Decode JSON into models using Codable and JSONDecoder
- • Read HTTP status codes via HTTPURLResponse
- • Treat 2xx as success and reject other ranges
- • Handle URLError and DecodingError separately
- • Write clean async throws fetching functions
📊 The Networking Pieces
| Piece | Role |
|---|---|
| URL | Where to fetch from |
| data(from:) | Async fetch, returns (Data, URLResponse) |
| HTTPURLResponse | Exposes the statusCode |
| Codable | Model that maps to JSON |
| JSONDecoder | Turns Data into your model |
1️⃣ Fetch and Decode
The core flow is three lines: build a URL, await the data, and decode it into a Codable model whose properties match the JSON keys.
import Foundation
// A Codable model mirrors the JSON shape you expect back.
struct Post: Codable {
let id: Int
let title: String
}
// async throws: it awaits the request and forwards any failure.
func loadPost() async throws -> Post {
let url = URL(string: "https://example.com/posts/1")!
// data(from:) returns (Data, URLResponse) and can throw a URLError.
let (data, _) = try await URLSession.shared.data(from: url)
// Decode the JSON bytes into our Post type.
let post = try JSONDecoder().decode(Post.self, from: data)
return post
}
// Calling it:
// let post = try await loadPost()
// print(post.title)Your turn. Fill in the protocol, the await keyword, and the decoder type.
// 🎯 YOUR TURN — replace each ___ then run it in SwiftFiddle.
import Foundation
struct User: ___ { let id: Int; let name: String } // 👉 protocol for JSON decoding
func loadUser() async throws -> User {
let url = URL(string: "https://example.com/user")!
let (data, _) = try ___ URLSession.shared.data(from: url) // 👉 keyword to wait
let user = try JSON___().decode(User.self, from: data) // 👉 the decoder type
return user
}
// ✅ Once filled in, this compiles and decodes a User from JSON.2️⃣ Check the Status Code
A 404 or 500 still returns Data rather than throwing, so always cast to HTTPURLResponse and confirm the code is in the 200..<300 range before trusting the body.
import Foundation
func fetchChecked(_ url: URL) async throws -> Data {
let (data, response) = try await URLSession.shared.data(from: url)
// Cast to HTTPURLResponse to read the status code.
guard let http = response as? HTTPURLResponse else {
throw URLError(.badServerResponse)
}
// 200–299 means success. Anything else is treated as a failure.
guard (200..<300).contains(http.statusCode) else {
print("Bad status: \(http.statusCode)")
throw URLError(.badServerResponse)
}
return data
}3️⃣ Handle Errors
Networking can fail in two ways: the request itself (URLError) or the parsing (DecodingError). Catch them separately so you can show the right message.
import Foundation
struct Item: Codable { let name: String }
func loadItem() async {
let url = URL(string: "https://example.com/item")!
do {
let (data, _) = try await URLSession.shared.data(from: url)
let item = try JSONDecoder().decode(Item.self, from: data)
print("Got: \(item.name)")
} catch let error as DecodingError {
print("Bad JSON shape: \(error)")
} catch let error as URLError {
print("Network problem: \(error.code)")
} catch {
print("Other error: \(error)")
}
}Now you try. Fill in the error-handling keywords.
// 🎯 YOUR TURN — handle failures.
import Foundation
struct Quote: Codable { let text: String }
func loadQuote() async {
let url = URL(string: "https://example.com/quote")!
___ { // 👉 keyword that begins error handling
let (data, _) = try await URLSession.shared.data(from: url)
let q = try JSONDecoder().decode(Quote.self, from: data)
print(q.text)
} ___ { // 👉 keyword that handles the thrown error
print("Failed: \(error)")
}
}
// ✅ Expected: prints the quote on success, or "Failed: ..." on error.Pro Tips
- 💡 Always check the status code. A non-2xx response still returns bytes — don't assume success.
- 💡 Match JSON keys exactly, or use CodingKeys / keyDecodingStrategy to bridge naming.
- 💡 Keep fetching off the main thread. await frees the thread automatically.
- 💡 Surface errors to the user, distinguishing "no connection" from "bad data".
Common Errors (and the fix)
- "keyNotFound" DecodingError — a JSON key doesn't match your property; add CodingKeys or fix the name.
- "typeMismatch" DecodingError — e.g. a number arrived as a String; align the property type.
- Treating a 404 as success — you skipped the status check; cast to HTTPURLResponse.
- "Call can throw but is not marked with 'try'" — add try before await on the request and the decode.
📋 Quick Reference
| Step | Code | Result |
|---|---|---|
| Build URL | URL(string: "…")! | an endpoint |
| Fetch | try await …data(from:url) | (Data,Response) |
| Status | resp as? HTTPURLResponse | statusCode |
| Decode | JSONDecoder().decode(T.self,…) | a model |
| Errors | do { try … } catch { … } | handled |
Mini-Challenge: Fetch and Validate
Write a full fetch that checks the status code and decodes a Todo, then report whether it's completed.
// 🎯 MINI-CHALLENGE: Fetch and validate
// 1. Define a Codable struct 'Todo' with id: Int and completed: Bool.
// 2. Write func loadTodo() async throws -> Todo that:
// - awaits URLSession.shared.data(from:) for a URL,
// - casts the response to HTTPURLResponse and checks 200..<300,
// - decodes the Data into a Todo with JSONDecoder.
// 3. Call it inside a do/catch and print whether the todo is completed.
//
// ✅ Example: "Todo 1 completed: true"
// your code here🎉 Lesson Complete!
- ✅ URLSession.shared.data(from:) fetches with async/await
- ✅ It returns (Data, URLResponse) and can throw
- ✅ Decode with JSONDecoder into a Codable model
- ✅ Cast to HTTPURLResponse and check the 2xx range
- ✅ Handle URLError and DecodingError separately
Practice quiz
Which URLSession method fetches data using async/await?
- URLSession.shared.data(from:)
- URLSession.fetch()
- URLSession.get()
- URLSession.load()
Answer: URLSession.shared.data(from:). data(from:) is the async method that returns (Data, URLResponse).
What does URLSession.shared.data(from:) return?
- Just Data
- A String
- A tuple of (Data, URLResponse)
- An Int status code
Answer: A tuple of (Data, URLResponse). It returns a tuple containing the response Data and the URLResponse.
Which protocol must a type adopt to be decoded from JSON?
- Hashable
- Decodable (or Codable)
- Equatable
- Identifiable
Answer: Decodable (or Codable). Decodable enables decoding; Codable is Decodable plus Encodable.
Which object turns JSON Data into a Swift value?
- JSONSerialization only
- JSONEncoder
- PropertyListDecoder
- JSONDecoder
Answer: JSONDecoder. JSONDecoder().decode(_:from:) decodes JSON Data into a Decodable type.
To read the HTTP status code, you cast the response to...
- URLRequest
- HTTPURLResponse
- URLComponents
- URLCache
Answer: HTTPURLResponse. Casting URLResponse to HTTPURLResponse exposes the statusCode.
Which status code range generally indicates success?
- 100–199
- 400–499
- 200–299
- 500–599
Answer: 200–299. 2xx codes indicate a successful HTTP response.
How do you handle errors from data(from:)?
- Use try/catch because it can throw
- Ignore them
- Use a guard with no try
- They never fail
Answer: Use try/catch because it can throw. data(from:) is async throws, so you call it with try inside a do/catch.
What does a URLError typically represent?
- A decoding mistake
- A nil optional
- A type cast error
- A networking failure such as no connection
Answer: A networking failure such as no connection. URLError describes networking-level failures like timeouts or no internet.
What does JSONDecoder throw on a shape mismatch?
- Nothing
- A DecodingError
- A URLError
- A fatalError
Answer: A DecodingError. Mismatched JSON throws a DecodingError such as keyNotFound or typeMismatch.
Why mark a fetching function async throws?
- For styling
- To run on the main thread
- So it can await the request and propagate failures
- To make it Sendable
Answer: So it can await the request and propagate failures. async lets it await the network call; throws lets it forward errors to the caller.
Continue this course
- Previous: Advanced Concurrency: async let, TaskGroup & Actors
- Next: Persistence with Core Data — Managed objects, contexts, fetch requests and @FetchRequest
Frequently asked questions
How do I make a network request with async/await?
Use URLSession.shared.data(from: url), which is an async throws method. Call it with try await inside an async function. It returns a tuple (Data, URLResponse): the raw bytes and metadata about the response. Because it is throwing, wrap the call in a do/catch or mark your function throws so the error propagates.
How do I turn JSON into a Swift type?
Define a struct that conforms to Decodable (or Codable) whose properties match the JSON keys, then call JSONDecoder().decode(MyType.self, from: data). The decoder maps JSON fields to your properties by name. If the JSON shape does not match, it throws a DecodingError describing the mismatch, such as a missing key or wrong type.
How do I read the HTTP status code?
data(from:) returns a URLResponse, but status codes live on HTTPURLResponse. Cast it with response as? HTTPURLResponse, then read its statusCode property. Treat 200 to 299 as success and throw or surface an error for other ranges, since a 404 or 500 still returns Data rather than throwing a URLError.
What is the difference between a URLError and a DecodingError?
A URLError is a networking-level failure: no internet, a timeout, a bad host, or a non-success situation you map to .badServerResponse. A DecodingError happens after the bytes arrive, when JSONDecoder cannot fit the JSON into your type. Catching them separately lets you show the user the right message for each kind of problem.
Why mark the function async throws?
async lets the function await the network call without blocking the thread, so the UI stays responsive. throws lets it forward both URLErrors from the request and DecodingErrors from parsing to the caller, who can decide how to handle them. Together they make networking code read like straight-line code while still handling failure cleanly.