Swift
NSURLSession
synchronous HTTP request
iOS development
Swift programming

Can I somehow do a synchronous HTTP request via NSURLSession in Swift

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

URLSession does not offer a normal synchronous API, and that is intentional. Network I/O is slow and unpredictable, so Apple’s networking stack is built around asynchronous execution rather than blocking the calling thread.

If what you really want is sequential-looking code, modern Swift already gives you that through async and await. In most cases, that is the right replacement for the old idea of a synchronous request.

Use the standard asynchronous API

The classic URLSession style uses a completion handler:

swift
1import Foundation
2
3let url = URL(string: "https://httpbin.org/get")!
4
5URLSession.shared.dataTask(with: url) { data, response, error in
6    if let error = error {
7        print("Request failed: \(error)")
8        return
9    }
10
11    let body = String(data: data ?? Data(), encoding: .utf8) ?? ""
12    print(body)
13}.resume()

This is the design the framework expects. The call returns immediately, and the response arrives later through the callback.

Prefer async and await in modern Swift

If you want code that reads top to bottom, use Swift concurrency instead of trying to force URLSession into a blocking API.

swift
1import Foundation
2
3func fetchBody() async throws -> String {
4    let url = URL(string: "https://httpbin.org/get")!
5    let (data, _) = try await URLSession.shared.data(from: url)
6    return String(data: data, encoding: .utf8) ?? ""
7}
8
9Task {
10    do {
11        let body = try await fetchBody()
12        print(body)
13    } catch {
14        print("Request failed: \(error)")
15    }
16}

This feels synchronous from the caller's point of view, but it does not block the main thread.

You can block a thread, but it is a workaround

If you truly must bridge async networking into a blocking API, a semaphore can do it. This is usually a legacy workaround and should not be your default design.

swift
1import Foundation
2
3func blockingFetch(url: URL) -> Result<Data, Error> {
4    let semaphore = DispatchSemaphore(value: 0)
5    var result: Result<Data, Error>!
6
7    URLSession.shared.dataTask(with: url) { data, _, error in
8        if let error = error {
9            result = .failure(error)
10        } else {
11            result = .success(data ?? Data())
12        }
13        semaphore.signal()
14    }.resume()
15
16    semaphore.wait()
17    return result
18}

This blocks the current thread until the request finishes. It should never be used on the main thread of an iOS app.

Why synchronous blocking is usually the wrong goal

Blocking network code creates several problems:

  • the UI can freeze
  • cancellation becomes awkward
  • timeouts and retries become harder to model
  • deadlocks become easier to introduce in wrapper code

The requirement often turns out to be "I want simple control flow," not "I want to block a thread." async and await solve the first problem without creating the second.

When a blocking wrapper may be acceptable

In a small command-line tool or test harness, blocking a background thread can be tolerable. In an iOS app, especially in UI code, it is almost always the wrong tradeoff.

If older APIs in your codebase expect a synchronous result, the better long-term fix is usually to push the async boundary upward and let callers become async too.

Common Pitfalls

The most common mistake is using a semaphore-based wrapper on the main thread. That can freeze the app and trigger watchdog termination.

Another issue is treating "sequential-looking code" as proof that you need synchronous networking. In Swift, structured concurrency already gives you sequential control flow without blocking.

Developers also sometimes expose blocking networking APIs from reusable libraries. That makes it harder for callers to cancel work or integrate with concurrency later.

Finally, do not forget that URLSession callbacks and async APIs already handle networking in the intended platform model. Fighting that design usually makes the code worse, not simpler.

Summary

  • 'URLSession is designed for asynchronous networking, not synchronous blocking.'
  • Use completion handlers or, preferably, async and await for sequential-looking code.
  • A semaphore can simulate synchronous behavior, but it is a workaround.
  • Never block the main thread waiting for a network response in an iOS app.
  • If synchronous networking seems necessary, reconsider the API boundary first.

Course illustration
Course illustration

All Rights Reserved.