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

📊 The Networking Pieces

PieceRole
URLWhere to fetch from
data(from:)Async fetch, returns (Data, URLResponse)
HTTPURLResponseExposes the statusCode
CodableModel that maps to JSON
JSONDecoderTurns 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

Common Errors (and the fix)

📋 Quick Reference

StepCodeResult
Build URLURL(string: "…")!an endpoint
Fetchtry await …data(from:url)(Data,Response)
Statusresp as? HTTPURLResponsestatusCode
DecodeJSONDecoder().decode(T.self,…)a model
Errorsdo { 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!

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

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.