Changed my code from completion to async and with the same decoding method i get this error

151 Views Asked by At

I had this code before using completion and it worked and still works perfectly:

func fetchUser(withUid uid: String, completion: @escaping(UserModel) -> Void) {
    Firestore.firestore().collection("users")
        .document(uid)
        .getDocument { snapshot, _ in
            guard let snapshot = snapshot else {return}
            
            guard let user = try? snapshot.data(as: UserModel.self) else {return}
            
            completion(user)
        }
}

but when i switched to async / await i get this error:

Cannot get keyed decoding container -- found null value instead.

With this code:

func fetchUser(withUid uid: String) async throws -> UserModel {
    do {
        let snapshot = try await Firestore.firestore().collection("users").document(uid).getDocument()
        do {
            let user = try snapshot.data(as: UserModel.self)
            return user
        } catch {
            throw(error)
        }
    } catch {
        throw(error)
    }
}

Any tips on where i went wrong?

Switched to async code and got an error with decoding

1

There are 1 best solutions below

2
Rob On

We are comparing apples to oranges here, because your completion handler rendition is not reporting errors, but rather just doing an immediate return without calling the completion handler. So you might be failing in the completion handler pattern without ever knowing, which is what I suspect is happening here.

Here is the standard completion handler pattern with a Result type that handles both .success and .failure:

func fetchUser(withUid uid: String, completion: @escaping (Result<UserModel, Error>) -> Void) {
    Firestore.firestore().collection("users")
        .document(uid)
        .getDocument { snapshot, error in
            guard let snapshot, error == nil else {
                completion(.failure(error ?? FirestoreErrorCode(.notFound)))
                return
            }

            do {
                let user = try snapshot.data(as: UserModel.self)
                completion(.success(user))
            } catch {
                completion(.failure(error))
            }
        }
}

With completion handlers, you want to make sure every path of execution calls the completion handler closure. (As an aside, that nil-coalescing operator, ??, with .notFound probably is not needed, because I believe that if snapshot is nil, there must be an error, but I always try to be defensive in my error handling code.)

Anyway, you would then have a switch statement on the result:

fetchUser(withUid: uid) { result in
    switch result {
    case .failure(let error):
        // update UI with `error` here

    case .success(let user):
        // use `user` here
    }
}

For more information, see Writing Failable Asynchronous APIs.


The async rendition could be simplified to:

func fetchUser(withUid uid: String) async throws -> UserModel {
    try await Firestore.firestore()
        .collection("users")
        .document(uid)
        .getDocument()
        .data(as: UserModel.self)
}

As you can see, there is no need for any do-catch blocks in the above, if all you plan on doing is rethrowing the error. The try will automatically throw the error for you.

Anyway, you can then call it with:

do {
    let user = try await fetchUser(withUid: uid)
    // use `user` here
} catch {
    // update UI with `error` here
}