What am I doing wrong in my attempts to use an async routine?

Ugh! Looks like the formatting barfed - will try again later... OK, it's fixed now...

Here comes a block containing both my "problem code" and a description of what I'm having trouble with - It's ready to copy/paste into an empty XCode project file (I'm running XCode 15.3) and compile (but will not actually run properly because it's got a fake URL and is intentionally missing some "Yeah, server, you can talk to this guy" authorization info that the conditions of my access to the server forbids me to share) - My question/problem(s) are comments strategically placed in the code.

// What follows is a severely "chopped down for brevity", and "sensitive info scrubbed to avoid breaking my NDA" version of my current "problem child" code.
// FWIW: I'm working on a 2020 iMac 27" with 72GB RAM and more than 10TB of spinning disk besides the built-in 256GB SSD (I believe in having "room to grow"),
// running the latest Sonoma (MacOS 14.3.1), using the newest Xcode/Swift (15.3 and 5.10, respectively) I know about, and targeting/testing against 14.3.

import SwiftUI

// Real URL redacted, fake used to get a file that will compile without complaining.
let DataURLString = "HTTPS://it.should.be/fairly/obvious/that/this/is/a/fake/URL/to/allow/posting/a/compilable/code-chunk/without/violating/the/NDA/covering/my/use/of/the/real/URL"
// Note: The real URL *ALWAYS* works correctly (unless the host, which I don't control, is down, or I don't have a live network connection - it's clearly NOT a connectivity issue)

var GlobalTemp:(Data, URLResponse) = (Data(), URLResponse())

@main struct MainApp: App {
    var InventoryData:(Data, URLResponse) = (Data(), URLResponse())
    var CurrentInventory:InventoryItems = []
    
    init() {
        // The very first thing that needs to happen when the program starts up is I need to grab the current inventory data, which the remote server will cheerfully hand me as a wad of JSON. Without this data,
        // running the program at all is an utterly pointless exercise in futility, since absolutely everything the program does revolves around the inventory data. (as well as the program output being strongly
        // influenced by what parts of the data we're actually looking at at any given moment) This is complicated by the fact that, besides the actual data the server hands me, I also need some data that only
        // comes as part of the URLResponse headers, and the only way (that I've found so far, anyway) to get at the headers, as well as the actual data, is to do the data-grab via a URLSession metyhod, all of
        // whose relevant methods seem to be asynchronous. Which in turn means that simply doing the obvious "try await URLSession.shared.data(from:targetURL)" after setting up the URL gets me the compile-time
        // error "'async' call in a function that does not support concurrency". Adding the suggested "async" to the init() declaration makes things worse - With that in place, I get a new compile-time error:
        // "Type 'MainApp' does not conform to protocol 'App'". AUGH! OK, pain in the rump, but if I'm not misunderstanding the docs, I should be able to ditch the "async" on init(), "wrap" the call I need in one
        // of these new "Task {...}" thingies, and Poof! Bob's yer uncle! Or so I thought...
        print("Execution begins.")
        print("MainApp's init() entered - about to execute the 'Task' block")
        Task {
            await FetchInventory() // The problem *APPEARS* to be here - It compiles properly, but DOES NOT seem to properly "await" as the docs suggest... See the "NO CRASH" and "CRASH" run log blocks which follow the
                                   // code for the "full report".
            print("Within init()'s Task{}: Data = \(GlobalTemp.0)")
            print("Within init()'s Task{}: URLResponse = \(GlobalTemp.1)")
        }
        print("Back from 'Task' withGlobalTemp = \(GlobalTemp)")
        InventoryData = GlobalTemp // Copy the data into local-to-us storage
        print("InventoryData = \(InventoryData)") // Show it off as debugging output so we know we've got "the good stuff"
        // When the "CurrentInventory = try!..." line is commented out, things work exactly as expected, with GlobalTemp and InventoryData being two copies of whatever data the server sent back, along
        // with the pertinent URLResponse info, as can be seen in the "NO CRASH" block attached down below.
        //
        // Uncommenting the "CurrentInventory = try!..." line *ALWAYS* crashes, and always the same way as can be seen in the "CRASH" block towards the end.
        // This isn't terribly surprising, considering the fact that the "Task" seems to "finish awaiting" before FetchInventory() gets a chance to run, as can be seen by inspecting the "CRASH" block also attached below.
        //
        // I was under the impression (mistaken?) that doing an "await" on an async routine is supposed to cause the current execution to suspend/block (terminology seems to be dependent on which text/tutorial one is reading)
        // until the task completes? As can be easily seen in both the "CRASH" and "NO CRASH" blocks, this is apparently NOT what's actually happening. Why?
        CurrentInventory = try! JSONDecoder().decode(InventoryItems.self, from: InventoryData.0) // <----- Crash happens here anytime this line isn't commented out.
        
        // If the line above is "hand fed" valid data (rather than trying to load it from a URL) the result of this next line is debug representation of the an array of structs that hold the de-JSONized version of the data,
        // exactly as intended/expected.
        print("CurrentInventory = \(CurrentInventory)")
    }
    
    var body: some Scene {
        WindowGroup {
            // MyPicker() // Defined elsewhere - works fine when tested with dummy data, commented out to get a clean compile without needing to drag all of the UI baggage into this post.
        }
    }
}

// This should be "fire-and-forget" code, ideally suited for async/background work - it's going to finish eventually, no matter what, and even if it encounters an error, things can still proceed and be handled by other
// "Oops, something went wrong, try to recover from it" code that's been exised for the sake of brevity in this posting.
func FetchInventory() async {
    print("FetchInventory() called asynchronously")
    do {
        let targetURL = URL(string: DataURLString)!
        GlobalTemp = try await URLSession.shared.data(from:targetURL) // Note that GlobalTemp is set up so as to capture (rather than the usual "silently drop 'em on the floor" behavior) the URLResponse headers -
                                                                      // I need several pieces of data, including the server date/time, that the server supplies, but only as URLResponse headers.
        print("FetchInventory() about to exit normally. (No error, so valid Data and the URLResponse have been loaded into GlobalTemp)")
    } catch {
        // Something borked - Return empty shells of Data and URLResponse so that the "show it to user" code can sniff it and say "Nothin' there to show, boss!" if something went wrong, or "Here ya go, boss!" and put it on
        // screen if there is.
        print("FetchInventory() about to exit through catch branch (AKA, an error occurred - exactly what kind of error is irrelevant at this point - we only care that it happened)")
        GlobalTemp.0 = Data() // Fake up an empty Data object to facilite the "detect a problem and attempt recovery" logic.
        GlobalTemp.1 = URLResponse() // Same thing - fake up an empty URLResponse object
    }
}

// Here are the "CRASH" and "NO CRASH" blocks I've mentioned already...

/* NO CRASH
   The text between '*****' markers is the near-verbatim (give or take clearly marked NDA-compliance redactions, plus my notes) debugging output from running with the "CurrentInventory = try!..." line
   in "init()" commented out, with my notes in the form "<----- something I say" at the ends of applicable lines.
*****
 Execution begins.
 MainApp's init() entered - about to execute the 'Task' block                                 <----- So far, so good...
 Back from 'Task' withGlobalTemp = (0 bytes, <NSURLResponse: 0x600001320f00> { URL: (null) }) <----- But what's this?!?!? Notice that FetchInventory() HAS NOT run yet, but we're already back???
 InventoryData = (0 bytes, <NSURLResponse: 0x600001320f00> { URL: (null) })                   <----- Of COURSE it's empty! FetchInventory() still hasn't run - we should still be sitting blocked - shouldn't we?
 CurrentInventory = []                                                                        <----- Same as previous line - Unless I misunderstand, we should still be blocked!
 FetchInventory() called asynchronously                                                       <----- FINALLY! FetchInventory fires up...
 FetchInventory() about to exit normally. (No error, so valid Data and the URLResponse have been loaded into GlobalTemp) <----- Exactly as should be...
 Within init()'s Task{}: Data = 69316 bytes                                                   <----- Byte count looks pretty reasonable - it can be highly variable. Dumping it shows the expected info
 Within init()'s Task{}: URLResponse = <NSHTTPURLResponse: 0x600000043700> { URL: REDACTED FOR NDA COMPLIANCE } { Status Code: 200, Headers {
 "Cache-Control" =     (
     "public,max-age=30"
 );
 "Content-Encoding" =     (
     gzip
 );
 "Content-Type" =     (
     "application/json; charset=utf-8"
 );
 Date =     (
     "Sat, 23 Mar 2024 00:54:18 GMT"
 );
 "Set-Cookie" =     (
     "REDACTED FOR NDA COMPLIANCE",
     "REDACTED FOR NDA COMPLIANCE"
 );
 "Strict-Transport-Security" =     (
     "max-age=2592000"
 );
 Vary =     (
     "host,Accept-Encoding"
 );
 "request-context" =     (
     "REDACTED FOR NDA COMPLIANCE"
 );
 } }       <----- So it's obviously getting both the data and the headers, but for some reason, it's doing so AFTER the code that was supposed to wait for it to arrive has apparently continued without waiting as expected.
 *****
 */


/* CRASH */
/* And here, we have the same debugging output, but from a run with the only difference being that the "CurrentInventory = Try!..."  call in "init()" *IS NOT* commented out: */
/*
*****
 Execution begins.
 MainApp's init() entered - about to execute the 'Task' block
 Back from 'Task' withGlobalTemp = (0 bytes, <NSURLResponse: 0x600001c3a7c0> { URL: (null) }) <----- Again, we're back too soon - FetchInventory() hasn't run yet. Why isn't it waiting like it should?!?!?
 InventoryData = (0 bytes, <NSURLResponse: 0x6000038e3980> { URL: (null) })
 MainApp.swift:53: Fatal error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: Optional(Error Domain=NSCocoaErrorDomain Code=3840 "Unexpected end of file" UserInfo={NSDebugDescription=Unexpected end of file})))
 *****           <----- Hey! What gives! FetchInventory never fired! (or blocked!) No wonder the decoding failed!!!
 
 
Notice how in both cases, execution continues before FetchInventory() gets a chance to execute? This seems to be the heart of the problem...
Why isn't the async task "awaiting"/blocking as the docs say it should?!?!? I'm getting some nasty knots on my head from beating it against this "nifty" brick wall... HELP!?!?!?!
 
Oh, and before anybody tells me "You're doing it wrong, you should put the data straight into InventoryData", trust me, I wanted to. But that seems to be a no-go - The compile-time error I got when I tried going that route
instead of trying to load it to "GlobalTemp" and copying from there was "Instance member 'FetchInventory' cannot be used on type 'MainApp'", so that idea went sailing merrily out the window at about Mach 3 very early in
my attempts. This remains true whether FetchInventory() is a member function inside the declaration of MainApp, or standalone, as seen here.
*/

        Task { // #1
            await FetchInventory() // #4
            ... GlobalTemp = ... // #5 (after data is loaded)
        }
        InventoryData = GlobalTemp // #2
        CurrentInventory = try! JSONDecoder().decode(...) // #3

This is the order the statements are getting executed.
You need to do json decoding after await finished awaiting. And better do it without a global and camelCase the variables. Something like this:

        Task {
            let (data, response) = await FetchInventory()
            currentInventory = try! JSONDecoder().decode(..., from: data)
        }

You'd also need to manage "currentInventory" correctly (assuming your views are using it). See the recent thread with a similar setup.

1 Like

Task { } starts a task to run the closure you pass it. That task then runs concurrently with the current context. So the await within the task does cause the current execution to wait for foo() to finish, but the “current execution” for that purpose is the task, not the function that started the task.

Once you’ve started a task, you can wait for it to finish by calling get on the task reference. You do have to await that, though, which means you can’t just do it in your init, since that’s a synchronous context.

The asynchrony actually shows an inherent issue with what you’re trying to do: your UI is being brought up synchronously in response to a user action (here, apparently starting your app), but you won’t have the data until you load it, which is a network request and can take awhile. You need to actually handle the data not being present yet and decide what to show — it’s a real state that can happen in your UI. Once it’s there, you just need to set it and tell your UI to refresh.

5 Likes