Image
Build
Connect & operate
Design & teams
Start hereScope a build in one callBring a spec, a wireframe, or a paragraph. You leave with an architecture, a timeline, and a number.Book a scoping call
AI software
LLM & data systems
Vibe coding
Ready to ship?Put AI where the work isAgents, RAG, and private LLMs wired into the systems your team already uses — not a chatbot bolted to a homepage.Discuss an AI project
Domain firstWe learn your workflow before we model itRegulated, operational, or high-volume — the constraints belong in the schema, not in a training doc.Talk about your domain
Plan smarterEstimate before you commitCost ranges, scope templates, and the questions we ask in discovery — free, no form.Open the cost calculator
Real conversationsTalk with a technical leadNo SDR, no discovery gauntlet. The person on the call is the one who scopes the build.Book a call
Eric Lamanna
Author
Swift Networking with URLSession and Async Await — featured image
9/30/2026

Swift Networking with URLSession and Async Await

Networking on Apple platforms used to feel like juggling flaming bowling pins while riding a bicycle on a tightrope. With modern Swift, that circus act is retired. URLSession remains the sturdy workhorse for HTTP, and async and await turn clunky callback pyramids into clean, readable flows. In this guide, we will explore how these pieces fit together, how to design a dependable client, and how to avoid the common traps that nibble at your sanity.

If you are building anything that talks to an API, from a tiny utility to a full mobile app, this will help you ship faster without sacrificing correctness. Along the way we will keep things practical, a bit witty, and grounded in habits that scale in software development.

Why Async Await Changes the Game

From Callbacks to Clarity

Before concurrency keywords arrived, networking code often ballooned with completion handlers, capture lists, and hand-rolled state machines. Async functions simplify this. A single await reads like normal sequential code while still being non-blocking. You request data, you get data, and you handle it where you asked for it. This reduces cognitive load for you and your teammates, improves readability during code reviews, and makes bugs much easier to pinpoint.

Async/Await Flattens Three Nested Callbacks Into OneIllustrative code nesting depth for a 3-step chained network request3Completion-handlerchain1async/awaitchain

Structured Concurrency and Lifetimes

Structured concurrency encourages tasks that begin and end in predictable places. When your view model starts a request inside a Task, the lifetime of that work is clear. If the view disappears, you can cancel the task, and the operation unwinds gracefully. With callbacks, these lifetimes are slippery. With Task, TaskGroup, and withTimeout, they are explicit. Your networking becomes safer because there are fewer loose ends and fewer orphaned operations waiting to bite.

Cleaner Error Propagation

Error handling becomes direct. Instead of plumbing an Error? through nested closures, you use throws and try. The call site reflects possible failure, and Swift forces you to consider it. This pressure is healthy. It nudges you to define your error types and failure paths up front, which means fewer surprise crashes and clearer user feedback.

Building a Solid URLSession Foundation

Choosing the Right Session Configuration

URLSession offers default, ephemeral, and background configurations. For most API calls, default works well and leverages the system cache. Ephemeral is useful for sensitive data because it avoids writing to disk. Background sessions are designed for long-running uploads and downloads that should continue when your app is suspended. Picking the correct configuration early avoids awkward retrofits later, and it sets expectations for caching and persistence from day one.

Sensible Timeouts and Connectivity

Time is a product requirement as much as a technical one. Users will forgive a brief spin of a progress indicator, not a request that hangs forever. Configure timeoutIntervalForRequest and timeoutIntervalForResource to reflect what your app promises. Combine that with the system’s network path monitoring if you need to tailor behavior when the connection is constrained. Your app feels attentive when it fails fast with a friendly message instead of leaving people guessing.

A Minimal, Reusable Client

A small APIClient wrapper pays for itself. Centralize your base URL, default headers, JSON encoding and decoding, and request construction. Keep it boring. When every request goes through a single path, you get consistent logging, consistent retry behavior, and a single place to inject features like authentication or metrics. Boring code in the foundation frees your creativity at the edges.

Requesting, Decoding, and Validating

Crafting URLRequest With Care

A precise URLRequest is half the battle. Set HTTP methods deliberately, attach headers only when needed, and avoid guessing content types. If the server expects JSON, encode with JSONEncoder and specify the correct header. Make query items with URLComponents rather than string concatenation. These small habits prevent subtle bugs that waste hours and keep your requests predictable across environments.

Codable Without Surprises

Codable is a joy when your API contracts are clear. Define response models that match payloads closely and use custom CodingKeys where naming differs. Normalize dates with a known dateDecodingStrategy instead of hoping the default matches the server. When decoding fails, capture the raw response and the decoding error together. That pair makes debugging swift and decisive, instead of a guessing game.

HTTP Status Handling That Respects Reality

Do not treat any non-200 as the same failure. A 401 means you should refresh credentials. A 403 suggests the user lacks permissions. A 404 usually means your endpoint or parameters are off. A 500 points to server trouble, which might be recoverable with a retry. Map status codes to error cases that carry context. Your user messages can then be human, specific, and helpful, and your retry logic can be smarter than a blind loop.

Concurrency Patterns That Scale

Cancellation Is Not Optional

Cancellation is a first-class citizen in Swift concurrency. If the user pulls to refresh and then navigates away, cancel the in-flight work. Check Task.isCancelled at natural breakpoints, such as after receiving headers or between decoding steps. Cancellation keeps your app snappy, reduces wasted bandwidth, and prevents outdated results from popping into the UI at awkward moments.

Retrying With Backoff and Limits

Networks are noisy. A single transient failure should not define the experience. Add a measured retry strategy that targets only errors worth retrying, such as timeouts or certain 5xx responses. Include a cap on attempts and a backoff delay that grows modestly. This keeps your app resilient without turning it into a runaway re-request machine that annoys servers and users alike.

A Capped Backoff Recovers Almost Every Transient FailureIllustrative cumulative request success rate by retry attempt92%97%99%99.6%1st attempt+1 retry+2 retries+3 retries

Actors for Shared Resources

When you have shared state, such as an in-memory cache or a token store, place it behind an actor. This guards against data races and simplifies reasoning. The boundary is clear, access is serialized, and you can still perform networking from within by hopping to detached work when appropriate. Actors remove a whole category of heisenbugs that only appear on your most important demo.

Performance, Caching, and Observability

Lean on URLCache Before Rolling Your Own

HTTP caching is older than most of us, and it works. Respect cache-control headers from the server, and let URLCache do the lifting for GETs that are safe to reuse. When you must cache aggressively, compute sensible keys and pair them with validation using ETags or Last-Modified. Smart caching cuts latency, saves battery, and reduces load on your backend.

Streaming and Memory Footprint

Large payloads can sink your app if you load everything into memory. Prefer streaming when you can. The bytes variants in URLSession let you process data incrementally, which is helpful for media, logs, or very large JSON arrays that you want to parse piece by piece. Your app remains responsive, and you avoid the dreaded memory pressure spiral that ends with the system tapping out.

Streaming Keeps Memory Flat as Payload Size GrowsIllustrative peak memory when parsing a large JSON response14 MB4 MB10 MBresponse140 MB5 MB100 MBresponse700 MB6 MB500 MBresponseLoad fully into memoryStream + parse incrementally

Metrics and Logging You Will Actually Use

When mistakes happen, your logs ought to be a rescue rope, not a crossword puzzle. Log request IDs, URLs without sensitive parameters, status codes, and elapsed time. Add high-level metrics for success rates and latency percentiles. Tie those numbers back to releases so you can see when a regression slips in. Developers ship faster when they trust their telemetry, and product managers stop guessing about performance.

Authentication, Security, and Clean Boundaries

Tokens, Refresh, and Separation of Concerns

Authentication flows are easier when they are not mixed into every request. Isolate token acquisition and refresh into a small component that the client consults. Add a hook that attaches the token to requests that need it, and keep unauthenticated calls free of that machinery. When a 401 arrives, trigger a refresh exactly once, queue dependent requests, and proceed when you have a fresh token. This avoids thundering herds and brittle state.

TLS, App Transport Security, and Pinning

Apple platforms prioritize secure defaults. App Transport Security enforces HTTPS, which is perfect for modern APIs. Only relax it for specific development hosts when you must. Certificate pinning can raise your security posture, but it comes with operational cost when certificates rotate. If you pin, plan the rotation story now, not the night before a cert expires. Good intentions do not help when your app locks itself out of production.

Sensitive Data and Privacy

Be mindful about what you log and where you store it. Do not write access tokens or raw personal data to the console or to disk. Use the keychain for secrets, not UserDefaults. Scrub crash reports if they could contain payloads. These practices are not only kind to your users, they keep your app in good standing with platform policies.

Testing, Mocking, and Future-Proofing

Deterministic Tests With Protocols

Networking is an integration concern, but you can still carve out deterministic tests. Define a protocol that your APIClient conforms to, and provide a mock in tests. Feed the mock known responses, validate decoding, and assert logic without touching the network. This builds confidence fast, and it keeps your CI pipeline reliable.

Fixtures That Represent Real Life

Your fixtures should look like the real payloads you expect in production. Include fields you ignore, fields that are missing, and occasional spicy surprises such as a null where you hoped for a string. The goal is not to mirror every corner case but to exercise your decoding in conditions you will actually meet. A little pessimism now prevents frantic hotfixes later.

Designing for Change

APIs move. New fields appear, old ones disappear, and endpoints evolve. Code for this. Default optional properties conservatively, provide sensible fallback values, and avoid crashing on unknown cases. Keep your client small, with one responsibility per type, so you can adjust a single piece without detangling your whole app. Resilience is not an accident. It is a series of small, thoughtful choices.

Making It Delightful for Users

Progress, Placeholders, and Empathy

A tiny spinner and a helpful hint do more for perceived performance than you might expect. Show progress for long operations, use skeleton views while content loads, and be honest when something fails. Offer a retry button that actually retries, not one that simply reopens the same broken view. Your users will feel respected, which translates directly into higher engagement.

Offline Modes That Earn Their Keep

Offline support is not all or nothing. Cache the last known good data and present it with a clear label when connectivity is absent. Queue lightweight actions for later, and allow users to cancel them if priorities change. Even a modest offline story improves trust because it shows that your app understands the real world where signals fade and trains go into tunnels.

Accessibility and Internationalization

Networking-driven screens often carry dense information. Respect Dynamic Type, provide labels for interactive elements, and keep error messages concise. Consider date and number formats that adapt to locale. None of this is glamorous, but it turns good features into great ones for a wider audience, and it prevents rework when your product grows.

Conclusion

Swift’s async and await make networking feel natural, and URLSession supplies the muscle behind the scenes. When you combine structured concurrency with a focused client, careful request building, precise decoding, and respectful error handling, you end up with code that is pleasant to read and reliable in production. Add caching, sane retries, real cancellation, and transparent metrics, and your app will feel fast and dependable even when the network is not. Keep your security posture strong, test with intention, plan for change, and design your UI with empathy. Do these things consistently, and your networking layer will fade into the background where it belongs, quietly powering the features your users love.

Author
Eric Lamanna
Eric Lamanna is a Digital Sales Manager with a strong passion for software and website development, AI, automation, and cybersecurity. With a background in multimedia design and years of hands-on experience in tech-driven sales, Eric thrives at the intersection of innovation and strategy—helping businesses grow through smart, scalable solutions. He specializes in streamlining workflows, improving digital security, and guiding clients through the fast-changing landscape of technology. Known for building strong, lasting relationships, Eric is committed to delivering results that make a meaningful difference. He holds a degree in multimedia design from Olympic College and lives in Denver, Colorado, with his wife and children.