Repository: firebase/firebase-ios-sdk (language: cpp)

Issue:
FirebaseStorage.StorageError provides limited information
### Description

One of my TestFlight users consistently gets an error when using the FirebaseStorage API:
`StorageReference.writeAsync()`

Logging this error provides little insights since the localizedDescription is
`The operation couldn’t be completed. (FirebaseStorage.StorageError error 0.)`

Unpacking the error case and logging the `.unknown(String)` produces
`An unknown error occurred, please check the server response.`

The server response isn't on the error however since its lost when the Objective-C `NSError` is converted the Swift enum via `swiftConvert(objcError:)`.

Would it be possible to instead make `StorageError` conform to [`CustomNSError`](https://developer.apple.com/documentation/foundation/customnserror) and expose the `userInfo` in Swift? Would love to debug this without rewriting a significant portion of my code in Objective-C.

### Reproducing the issue

Trying to figure this out myself.

### Firebase SDK Version

10.5.0

### Xcode Version

14.2

### Installation Method

Swift Package Manager

### Firebase Product(s)

Storage

### Targeted Platforms

iOS

### Relevant Log Output

_No response_

### If using Swift Package Manager, the project's Package.resolved

```json
{
  "pins" : [
    {
      "identity" : "abseil-cpp-swiftpm",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/abseil-cpp-SwiftPM.git",
      "state" : {
        "revision" : "583de9bd60f66b40e78d08599cc92036c2e7e4e1",
        "version" : "0.20220203.2"
      }
    },
    {
      "identity" : "boringssl-swiftpm",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/boringssl-SwiftPM.git",
      "state" : {
        "revision" : "dd3eda2b05a3f459fc3073695ad1b28659066eab",
        "version" : "0.9.1"
      }
    },
    {
      "identity" : "firebase-ios-sdk",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/firebase-ios-sdk",
      "state" : {
        "revision" : "f567ed9a2b30e29159df258049a9c662c517688e",
        "version" : "10.5.0"
      }
    },
    {
      "identity" : "googleappmeasurement",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/GoogleAppMeasurement.git",
      "state" : {
        "revision" : "9a09ece724128e8d1e14c5133b87c0e236844ac0",
        "version" : "10.4.0"
      }
    },
    {
      "identity" : "googledatatransport",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/GoogleDataTransport.git",
      "state" : {
        "revision" : "f6b558e3f801f2cac336b04f615ce111fa9ddaa0",
        "version" : "9.2.1"
      }
    },
    {
      "identity" : "googleutilities",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/GoogleUtilities.git",
      "state" : {
        "revision" : "0543562f85620b5b7c510c6bcbef75b562a5127b",
        "version" : "7.11.0"
      }
    },
    {
      "identity" : "grpc-ios",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/grpc/grpc-ios.git",
      "state" : {
        "revision" : "8440b914756e0d26d4f4d054a1c1581daedfc5b6",
        "version" : "1.44.3-grpc"
      }
    },
    {
      "identity" : "gtm-session-fetcher",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/gtm-session-fetcher.git",
      "state" : {
        "revision" : "5ccda3981422a84186387dbb763ba739178b529c",
        "version" : "2.3.0"
      }
    },
    {
      "identity" : "leveldb",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/leveldb.git",
      "state" : {
        "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b",
        "version" : "1.22.2"
      }
    },
    {
      "identity" : "mixpanel-swift",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/mixpanel/mixpanel-swift",
      "state" : {
        "branch" : "master",
        "revision" : "dfc52c9155f2e42fb69f98a2370647d28d7eebe1"
      }
    },
    {
      "identity" : "nanopb",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/firebase/nanopb.git",
      "state" : {
        "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692",
        "version" : "2.30909.0"
      }
    },
    {
      "identity" : "promises",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/google/promises.git",
      "state" : {
        "revision" : "ec957ccddbcc710ccc64c9dcbd4c7006fcf8b73a",
        "version" : "2.2.0"
      }
    },
    {
      "identity" : "swift-protobuf",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/apple/swift-protobuf.git",
      "state" : {
        "revision" : "0af9125c4eae12a4973fb66574c53a54962a9e1e",
        "version" : "1.21.0"
      }
    }
  ],
  "version" : 2
}
```
### If using CocoaPods, the project's Podfile.lock

<!--- Look below for instructions on how to share your Podfile.lock. --->

<details>
<summary>Expand <code>Podfile.lock</code> snippet</summary>
<br>

```yml

Replace this line with the contents of your Podfile.lock!

```

</details>



Discussion / hints:
I couldn't figure out how to label this issue, so I've labeled it for a human to triage. Hang tight.
Thanks for the report. We'll take a look in the next few weeks. In the meantime, let us know if you find anything more - and we'd be interested in a PR.
Errors from the server should actually be wrapped by `func error(withServerError serverError: NSError, ref: StorageReference)`.  if the error is not a server error, all the context should be in the Swift error. More examples and context in #10913.  If you're still blocked, we may need a reproducible example.
Sorry for the delayed response and thanks for adding a PR!

So the `Error` that is created via `error(withServerError:..` is actually great - it has all the info needed since it copies over the `userInfo` dictionary from the server `Error`. #10913 might actually not be necessary as all the info is carried over.

The loss in information comes when the wrapped `Error` is converted to the new `StorageError` in the `getResultCallback` method. I think a fix here would be to just not call `.swiftConvert(objcError:`. The case where there is no error in the callback can potentially leverage the new `StorageError` error type introduced.

I was actually able to work around this quite easily (staying in an async Swift context) by just using the sync APIs and wrapping them with `withCheckedThrowingContinuation` myself. This threw the wrapped `error(withServerError` error as opposed to the `StorageError` and had all the info needed in my logging to debug.

TL'DR
Server Error -> Wrapped Error -> StorageError. 
The first two are great - the third is missing info.
Thanks @jurgowski. I like the ideas of using `CustomNSError` and/or dropping `swiftConvert`. However, I'm not sure that can be done without breaking the ability to catch individual `StorageErrors` so it would be a breaking change. As a result, I've marked this for our next breaking change release.

I agree that the `underlyingError` change isn't absolutely necessary but it should be non-breaking and provide the underlying server Error in a more standard way.
I guess the main use case for `CustomNSError` is to make Swift errors easy to deal with in Obj-C code without custom bridging. It seems like in this case, the errors are coming from Obj-C and `StorageError` is a newly introduced error type (I didn't realize this when I first opened the issue). 

I agree that I don't think there is a way to make a change here that doesn't alter current behavior since `StorageError` is an enum.. However, I would expect that anyone catching `StorageError` is probably catching all errors generically. It may be possible to use the error directly in the failure case of the `Result` in `getResultCallback` (without converting it to `StorageError` via `swiftConvert`). As long as `StorageError` isn't deleted, things would still compile. 
Some WIP at https://github.com/firebase/firebase-ios-sdk/commit/3fae8e7ebc364985642a1000e89b2f663b5fc8ae

Fix patch:
diff --git a/FirebaseStorage/CHANGELOG.md b/FirebaseStorage/CHANGELOG.md
index 1baafc4bbdb..1703f2ae829 100644
--- a/FirebaseStorage/CHANGELOG.md
+++ b/FirebaseStorage/CHANGELOG.md
@@ -1,3 +1,9 @@
+# 11.0.0
+- [fixed] Updated error handling to support both Swift error enum handling and NSError error
+  handling. Some of the Swift enums have additional parameters which may be a **breaking** change.
+  There are additional NSError's for completeness, but nothing related to NSError handling is
+  breaking. (#13071, #10889, #13114)
+
 # 10.24.0
 - [fixed] `putFile` and `putFileAsync` now work in app extensions. A background session
    configuration is not used when uploading from an app extension (#12579).
diff --git a/FirebaseStorage/Sources/AsyncAwait.swift b/FirebaseStorage/Sources/AsyncAwait.swift
index 8fffded0e18..e38a2525711 100644
--- a/FirebaseStorage/Sources/AsyncAwait.swift
+++ b/FirebaseStorage/Sources/AsyncAwait.swift
@@ -66,7 +66,8 @@ public extension StorageReference {
       }
       uploadTask.observe(.failure) { snapshot in
         continuation.resume(with: .failure(
-          snapshot.error ?? StorageError.internalError("Internal Storage Error in putDataAsync")
+          snapshot.error ?? StorageError
+            .internalError(message: "Internal Storage Error in putDataAsync")
         ))
       }
     }
@@ -103,7 +104,8 @@ public extension StorageReference {
       }
       uploadTask.observe(.failure) { snapshot in
         continuation.resume(with: .failure(
-          snapshot.error ?? StorageError.internalError("Internal Storage Error in putFileAsync")
+          snapshot.error ?? StorageError
+            .internalError(message: "Internal Storage Error in putFileAsync")
         ))
       }
     }
@@ -137,7 +139,8 @@ public extension StorageReference {
       }
       downloadTask.observe(.failure) { snapshot in
         continuation.resume(with: .failure(
-          snapshot.error ?? StorageError.internalError("Internal Storage Error in writeAsync")
+          snapshot.error ?? StorageError
+            .internalError(message: "Internal Storage Error in writeAsync")
         ))
       }
     }
diff --git a/FirebaseStorage/Sources/Internal/StorageGetDownloadURLTask.swift b/FirebaseStorage/Sources/Internal/StorageGetDownloadURLTask.swift
index 2cca8acdca2..5fb5ca61ae1 100644
--- a/FirebaseStorage/Sources/Internal/StorageGetDownloadURLTask.swift
+++ b/FirebaseStorage/Sources/Internal/StorageGetDownloadURLTask.swift
@@ -74,10 +74,10 @@ class StorageGetDownloadURLTask: StorageTask, StorageTaskManagement {
              .jsonObject(with: data) as? [String: Any] {
             downloadURL = self.downloadURLFromMetadataDictionary(responseDictionary)
             if downloadURL == nil {
-              self.error = NSError(domain: StorageErrorDomain,
-                                   code: StorageErrorCode.unknown.rawValue,
-                                   userInfo: [NSLocalizedDescriptionKey:
-                                     "Failed to retrieve a download URL."])
+              self.error = StorageError.unknown(
+                message: "Failed to retrieve a download URL.",
+                serverError: [:]
+              ) as NSError
             }
           } else {
             self.error = StorageErrorCode.error(withInvalidRequest: data)
diff --git a/FirebaseStorage/Sources/Internal/StorageTokenAuthorizer.swift b/FirebaseStorage/Sources/Internal/StorageTokenAuthorizer.swift
index 217f8775116..bb195656cae 100644
--- a/FirebaseStorage/Sources/Internal/StorageTokenAuthorizer.swift
+++ b/FirebaseStorage/Sources/Internal/StorageTokenAuthorizer.swift
@@ -45,12 +45,7 @@ class StorageTokenAuthorizer: NSObject, GTMSessionFetcherAuthorizer {
           var errorDictionary = error.userInfo
           errorDictionary["ResponseErrorDomain"] = error.domain
           errorDictionary["ResponseErrorCode"] = error.code
-          errorDictionary[NSLocalizedDescriptionKey] =
-            "User is not authenticated, please authenticate" +
-            " using Firebase Authentication and try again."
-          tokenError = NSError(domain: "FIRStorageErrorDomain",
-                               code: StorageErrorCode.unauthenticated.rawValue,
-                               userInfo: errorDictionary)
+          tokenError = StorageError.unauthenticated(serverError: errorDictionary) as NSError
         } else if let token {
           let firebaseToken = "Firebase \(token)"
           request?.setValue(firebaseToken, forHTTPHeaderField: "Authorization")
diff --git a/FirebaseStorage/Sources/Result.swift b/FirebaseStorage/Sources/Result.swift
index 931794cbfb1..3a73da64f97 100644
--- a/FirebaseStorage/Sources/Result.swift
+++ b/FirebaseStorage/Sources/Result.swift
@@ -29,9 +29,11 @@ private func getResultCallback<T>(completion: @escaping (Result<T, Error>) -> Vo
     if let value {
       completion(.success(value))
     } else if let error {
-      completion(.failure(StorageError.swiftConvert(objcError: error as NSError)))
+      completion(.failure(error))
     } else {
-      completion(.failure(StorageError.internalError("Internal failure in getResultCallback")))
+      completion(.failure(StorageError.internalError(
+        message: "Internal failure in getResultCallback"
+      )))
     }
   }
 }
diff --git a/FirebaseStorage/Sources/Storage.swift b/FirebaseStorage/Sources/Storage.swift
index 3d547e28da3..09f211d3341 100644
--- a/FirebaseStorage/Sources/Storage.swift
+++ b/FirebaseStorage/Sources/Storage.swift
@@ -193,9 +193,9 @@ import FirebaseCore
     do {
       path = try StoragePath.path(string: url.absoluteString)
     } catch let StoragePathError.storagePathError(message) {
-      throw StorageError.pathError(message)
+      throw StorageError.pathError(message: message)
     } catch {
-      throw StorageError.pathError("Internal error finding StoragePath: \(error)")
+      throw StorageError.pathError(message: "Internal error finding StoragePath: \(error)")
     }
 
     // If no default bucket exists (empty string), accept anything.
@@ -205,7 +205,7 @@ import FirebaseCore
     // If there exists a default bucket, throw if provided a different bucket.
     if path.bucket != storageBucket {
       throw StorageError
-        .bucketMismatch("Provided bucket: `\(path.bucket)` does not match the Storage " +
+        .bucketMismatch(message: "Provided bucket: `\(path.bucket)` does not match the Storage " +
           "bucket of the current instance: `\(storageBucket)`")
     }
     return StorageReference(storage: self, path: path)
diff --git a/FirebaseStorage/Sources/StorageDownloadTask.swift b/FirebaseStorage/Sources/StorageDownloadTask.swift
index 8f9f0332393..91629bd632b 100644
--- a/FirebaseStorage/Sources/StorageDownloadTask.swift
+++ b/FirebaseStorage/Sources/StorageDownloadTask.swift
@@ -67,8 +67,7 @@ open class StorageDownloadTask: StorageObservableTask, StorageTaskManagement {
    * Cancels a task.
    */
   @objc open func cancel() {
-    let error = StorageErrorCode.error(withCode: .cancelled)
-    cancel(withError: error)
+    cancel(withError: StorageError.cancelled as NSError)
   }
 
   /**
diff --git a/FirebaseStorage/Sources/StorageError.swift b/FirebaseStorage/Sources/StorageError.swift
index d9e80dd31bd..f3e02340a2d 100644
--- a/FirebaseStorage/Sources/StorageError.swift
+++ b/FirebaseStorage/Sources/StorageError.swift
@@ -36,24 +36,17 @@ public let StorageErrorDomain: String = "FIRStorageErrorDomain"
   case downloadSizeExceeded = -13032
   case cancelled = -13040
   case invalidArgument = -13050
+  case bucketMismatch = -13051
+  case internalError = -13052
+  case pathError = -13053
 
   /**
-   * Creates a Firebase Storage error from a specific GCS error and FIRIMPLStorageReference.
+   * Creates a Firebase Storage error from a specific GCS error and StorageReference.
    * @param serverError Server error to wrap and return as a Firebase Storage error.
    * @param ref StorageReference which provides context about the request being made.
    * @return Returns a Firebase Storage error.
    */
   static func error(withServerError serverError: NSError, ref: StorageReference) -> NSError {
-    var errorCode: StorageErrorCode
-    switch serverError.code {
-    case 400: errorCode = .unknown
-    case 401: errorCode = .unauthenticated
-    case 402: errorCode = .quotaExceeded
-    case 403: errorCode = .unauthorized
-    case 404: errorCode = .objectNotFound
-    default: errorCode = .unknown
-    }
-
     var errorDictionary = serverError.userInfo
     errorDictionary["ResponseErrorDomain"] = serverError.domain
     errorDictionary["ResponseErrorCode"] = serverError.code
@@ -66,7 +59,29 @@ public let StorageErrorDomain: String = "FIRStorageErrorDomain"
     if let data = (errorDictionary["data"] as? Data) {
       errorDictionary["ResponseBody"] = String(data: data, encoding: .utf8)
     }
-    return error(withCode: errorCode, infoDictionary: errorDictionary)
+    let storageError = switch serverError.code {
+    case 400: StorageError.unknown(
+        message: "Unknown 400 error from backend",
+        serverError: errorDictionary
+      )
+    case 401: StorageError.unauthenticated(serverError: errorDictionary)
+    case 402: StorageError.quotaExceeded(
+        bucket: ref.path.bucket,
+        serverError: errorDictionary
+      )
+    case 403: StorageError.unauthorized(
+        bucket: ref.path.bucket,
+        object: ref.path.object ?? "<object-entity-internal-error>",
+        serverError: errorDictionary
+      )
+    case 404: StorageError.objectNotFound(
+        object: ref.path.object ?? "<object-entity-internal-error>", serverError: errorDictionary
+      )
+    default: StorageError.unknown(
+        message: "Unexpected \(serverError.code) code from backend", serverError: errorDictionary
+      )
+    }
+    return storageError as NSError
   }
 
   /** Creates a Firebase Storage error from an invalid request.
@@ -81,143 +96,123 @@ public let StorageErrorDomain: String = "FIRStorageErrorDomain"
     } else {
       requestString = "<nil request returned from server>"
     }
-    let invalidDataString = "Invalid data returned from the server:\(requestString)"
-    return error(
-      withCode: .unknown,
-      infoDictionary: [NSLocalizedFailureErrorKey: invalidDataString]
-    )
+    let invalidDataString = "Invalid data returned from the server: \(requestString)"
+    return StorageError.unknown(message: invalidDataString, serverError: [:]) as NSError
   }
+}
 
-  /**
-   * Creates a Firebase Storage error from a specific FIRStorageErrorCode while adding
-   * custom info from an optionally provided info dictionary.
-   */
-  static func error(withCode code: StorageErrorCode,
-                    infoDictionary: [String: Any]? = nil) -> NSError {
-    var dictionary = infoDictionary ?? [:]
-    var errorMessage: String
-    switch code {
+/// Firebase Storage errors
+public enum StorageError: Error, CustomNSError {
+  case unknown(message: String, serverError: [String: Any])
+  case objectNotFound(object: String, serverError: [String: Any])
+  case bucketNotFound(bucket: String)
+  case projectNotFound(project: String)
+  case quotaExceeded(bucket: String, serverError: [String: Any])
+  case unauthenticated(serverError: [String: Any])
+  case unauthorized(bucket: String, object: String, serverError: [String: Any])
+  case retryLimitExceeded
+  case nonMatchingChecksum
+  case downloadSizeExceeded(total: Int64, maxSize: Int64)
+  case cancelled
+  case invalidArgument(message: String)
+  case internalError(message: String)
+  case bucketMismatch(message: String)
+  case pathError(message: String)
+
+  // MARK: - CustomNSError
+
+  /// Default domain of the error.
+  public static var errorDomain: String { return StorageErrorDomain }
+
+  /// The error code within the given domain.
+  public var errorCode: Int {
+    switch self {
+    case .unknown:
+      return StorageErrorCode.unknown.rawValue
     case .objectNotFound:
-      let object = dictionary["object"] ?? "<object-entity-internal-error>"
-      errorMessage = "Object \(object) does not exist."
+      return StorageErrorCode.objectNotFound.rawValue
     case .bucketNotFound:
-      let bucket = dictionary["bucket"] ?? "<bucket-entity-internal-error>"
-      errorMessage = "Bucket \(bucket) does not exist."
+      return StorageErrorCode.bucketNotFound.rawValue
     case .projectNotFound:
-      let project = dictionary["project"] ?? "<project-entity-internal-error>"
-      errorMessage = "Project \(project) does not exist."
+      return StorageErrorCode.projectNotFound.rawValue
     case .quotaExceeded:
-      let bucket = dictionary["bucket"] ?? "<bucket-entity-internal-error>"
-      errorMessage =
-        "Quota for bucket \(bucket) exceeded, please view quota on firebase.google.com."
-    case .downloadSizeExceeded:
-      let total = "\(dictionary["totalSize"] ?? "unknown")"
-      let size = "\(dictionary["maxAllowedSize"] ?? "unknown")"
-      errorMessage = "Attempted to download object with size of \(total) bytes, " +
-        "which exceeds the maximum size of \(size) bytes. " +
-        "Consider raising the maximum download size, or using StorageReference.write"
+      return StorageErrorCode.quotaExceeded.rawValue
     case .unauthenticated:
-      errorMessage = "User is not authenticated, please authenticate using Firebase " +
-        "Authentication and try again."
+      return StorageErrorCode.unauthenticated.rawValue
     case .unauthorized:
-      let bucket = dictionary["bucket"] ?? "<bucket-entity-internal-error>"
-      let object = dictionary["object"] ?? "<object-entity-internal-error>"
-      errorMessage = "User does not have permission to access gs://\(bucket)/\(object)."
+      return StorageErrorCode.unauthorized.rawValue
     case .retryLimitExceeded:
-      errorMessage = "Max retry time for operation exceeded, please try again."
+      return StorageErrorCode.retryLimitExceeded.rawValue
     case .nonMatchingChecksum:
-      // TODO: replace with actual checksum strings when we choose to implement.
-      errorMessage = "Uploaded/downloaded object TODO has checksum: TODO " +
-        "which does not match server checksum: TODO. Please retry the upload/download."
+      return StorageErrorCode.nonMatchingChecksum.rawValue
+    case .downloadSizeExceeded:
+      return StorageErrorCode.downloadSizeExceeded.rawValue
     case .cancelled:
-      errorMessage = "User cancelled the upload/download."
-    case .unknown, .invalidArgument: // invalidArgument fell through in the old Objective-C code.
-      errorMessage = "An unknown error occurred, please check the server response."
+      return StorageErrorCode.cancelled.rawValue
+    case .invalidArgument:
+      return StorageErrorCode.invalidArgument.rawValue
+    case .internalError:
+      return StorageErrorCode.internalError.rawValue
+    case .bucketMismatch:
+      return StorageErrorCode.bucketMismatch.rawValue
+    case .pathError:
+      return StorageErrorCode.pathError.rawValue
     }
-    dictionary[NSLocalizedDescriptionKey] = errorMessage
-    return NSError(domain: StorageErrorDomain, code: code.rawValue, userInfo: dictionary)
   }
-}
-
-public enum StorageError: Error {
-  case unknown(String)
-  case objectNotFound(String)
-  case bucketNotFound(String)
-  case projectNotFound(String)
-  case quotaExceeded(String)
-  case unauthenticated
-  case unauthorized(String, String)
-  case retryLimitExceeded
-  case nonMatchingChecksum
-  case downloadSizeExceeded(Int64, Int64)
-  case cancelled
-  case invalidArgument(String)
-  case internalError(String)
-  case bucketMismatch(String)
-  case pathError(String)
 
-  static func swiftConvert(objcError: NSError) -> StorageError {
-    let userInfo = objcError.userInfo
-
-    switch objcError.code {
-    case StorageErrorCode.unknown.rawValue:
-      return StorageError.unknown(objcError.localizedDescription)
-    case StorageErrorCode.objectNotFound.rawValue:
-      guard let object = userInfo["object"] as? String else {
-        return StorageError
-          .internalError(
-            "Failed to decode object not found error: \(objcError.localizedDescription)"
-          )
-      }
-      return StorageError.objectNotFound(object)
-    case StorageErrorCode.bucketNotFound.rawValue:
-      guard let bucket = userInfo["bucket"] as? String else {
-        return StorageError
-          .internalError(
-            "Failed to decode bucket not found error: \(objcError.localizedDescription)"
-          )
-      }
-      return StorageError.bucketNotFound(bucket)
-    case StorageErrorCode.projectNotFound.rawValue:
-      guard let project = userInfo["project"] as? String else {
-        return StorageError
-          .internalError(
-            "Failed to decode project not found error: \(objcError.localizedDescription)"
-          )
-      }
-      return StorageError.projectNotFound(project)
-    case StorageErrorCode.quotaExceeded.rawValue:
-      guard let bucket = userInfo["bucket"] as? String else {
-        return StorageError
-          .internalError("Failed to decode quota exceeded error: \(objcError.localizedDescription)")
-      }
-      return StorageError.quotaExceeded(bucket)
-    case StorageErrorCode.unauthenticated.rawValue: return StorageError.unauthenticated
-    case StorageErrorCode.unauthorized.rawValue:
-      guard let bucket = userInfo["bucket"] as? String,
-            let object = userInfo["object"] as? String else {
-        return StorageError
-          .internalError(
-            "Failed to decode unauthorized error: \(objcError.localizedDescription)"
-          )
-      }
-      return StorageError.unauthorized(bucket, object)
-    case StorageErrorCode.retryLimitExceeded.rawValue: return StorageError.retryLimitExceeded
-    case StorageErrorCode.nonMatchingChecksum.rawValue: return StorageError
-      .nonMatchingChecksum
-    case StorageErrorCode.downloadSizeExceeded.rawValue:
-      guard let total = userInfo["totalSize"] as? Int64,
-            let maxSize = userInfo["maxAllowedSize"] as? Int64 else {
-        return StorageError
-          .internalError(
-            "Failed to decode downloadSizeExceeded error: \(objcError.localizedDescription)"
-          )
-      }
-      return StorageError.downloadSizeExceeded(total, maxSize)
-    case StorageErrorCode.cancelled.rawValue: return StorageError.cancelled
-    case StorageErrorCode.invalidArgument.rawValue: return StorageError
-      .invalidArgument(objcError.localizedDescription)
-    default: return StorageError.internalError("Internal error converting ObjC Error to Swift")
+  /// The default user-info dictionary.
+  public var errorUserInfo: [String: Any] {
+    switch self {
+    case let .unknown(message, serverError):
+      var dictionary = serverError
+      dictionary[NSLocalizedDescriptionKey] = message
+      return dictionary
+    case let .objectNotFound(object, serverError):
+      var dictionary = serverError
+      dictionary[NSLocalizedDescriptionKey] = "Object \(object) does not exist."
+      return dictionary
+    case let .bucketNotFound(bucket):
+      return [NSLocalizedDescriptionKey: "Bucket \(bucket) does not exist."]
+    case let .projectNotFound(project):
+      return [NSLocalizedDescriptionKey: "Project \(project) does not exist."]
+    case let .quotaExceeded(bucket, serverError):
+      var dictionary = serverError
+      dictionary[NSLocalizedDescriptionKey] =
+        "Quota for bucket \(bucket) exceeded, please view quota on firebase.google.com."
+      return dictionary
+    case let .unauthenticated(serverError):
+      var dictionary = serverError
+      dictionary[NSLocalizedDescriptionKey] = "User is not authenticated, please " +
+        "authenticate using Firebase Authentication and try again."
+      return dictionary
+    case let .unauthorized(bucket, object, serverError):
+      var dictionary = serverError
+      dictionary[NSLocalizedDescriptionKey] =
+        "User does not have permission to access gs://\(bucket)/\(object)."
+      return dictionary
+    case .retryLimitExceeded:
+      return [NSLocalizedDescriptionKey: "Max retry time for operation exceeded, please try again."]
+    case .nonMatchingChecksum:
+      // TODO: replace with actual checksum strings when we choose to implement.
+      return [NSLocalizedDescriptionKey: "Uploaded/downloaded object TODO has checksum: TODO " +
+        "which does not match server checksum: TODO. Please retry the upload/download."]
+    case let .downloadSizeExceeded(total, maxSize):
+      var dictionary: [String: Any] = ["totalSize": total, "maxAllowedSize": maxSize]
+      dictionary[NSLocalizedDescriptionKey] = "Attempted to download object with size of " +
+        "\(total) bytes, " +
+        "which exceeds the maximum size of \(maxSize) bytes. " +
+        "Consider raising the maximum download maxSize, or using StorageReference.write"
+      return dictionary
+    case .cancelled:
+      return [NSLocalizedDescriptionKey: "User cancelled the upload/download."]
+    case let .invalidArgument(message):
+      return [NSLocalizedDescriptionKey: message]
+    case let .internalError(message):
+      return [NSLocalizedDescriptionKey: message]
+    case let .bucketMismatch(message):
+      return [NSLocalizedDescriptionKey: message]
+    case let .pathError(message):
+      return [NSLocalizedDescriptionKey: message]
     }
   }
 }
diff --git a/FirebaseStorage/Sources/StorageReference.swift b/FirebaseStorage/Sources/StorageReference.swift
index 92a547b3f14..9e4f0c29c41 100644
--- a/FirebaseStorage/Sources/StorageReference.swift
+++ b/FirebaseStorage/Sources/StorageReference.swift
@@ -399,11 +399,9 @@ import Foundation
   open func list(maxResults: Int64,
                  completion: @escaping ((_: StorageListResult?, _: Error?) -> Void)) {
     if maxResults <= 0 || maxResults > 1000 {
-      completion(nil,
-                 NSError(domain: StorageErrorDomain,
-                         code: StorageErrorCode.invalidArgument.rawValue,
-                         userInfo: [NSLocalizedDescriptionKey:
-                           "Argument 'maxResults' must be between 1 and 1000 inclusive."]))
+      completion(nil, StorageError.invalidArgument(
+        message: "Argument 'maxResults' must be between 1 and 1000 inclusive."
+      ))
     } else {
       let fetcherService = storage.fetcherServiceForApp
       let task = StorageListTask(reference: self,
@@ -437,11 +435,9 @@ import Foundation
                  pageToken: String,
                  completion: @escaping ((_: StorageListResult?, _: Error?) -> Void)) {
     if maxResults <= 0 || maxResults > 1000 {
-      completion(nil,
-                 NSError(domain: StorageErrorDomain,
-                         code: StorageErrorCode.invalidArgument.rawValue,
-                         userInfo: [NSLocalizedDescriptionKey:
-                           "Argument 'maxResults' must be between 1 and 1000 inclusive."]))
+      completion(nil, StorageError.invalidArgument(
+        message: "Argument 'maxResults' must be between 1 and 1000 inclusive."
+      ))
     } else {
       let fetcherService = storage.fetcherServiceForApp
       let task = StorageListTask(reference: self,
@@ -584,11 +580,10 @@ import Foundation
   /// For maxSize API, return an error if the size is exceeded.
   private func checkSizeOverflow(task: StorageTask, maxSize: Int64) -> NSError? {
     if task.progress.totalUnitCount > maxSize || task.progress.completedUnitCount > maxSize {
-      return StorageErrorCode.error(withCode: .downloadSizeExceeded,
-                                    infoDictionary: [
-                                      "totalSize": task.progress.totalUnitCount,
-                                      "maxAllowedSize": maxSize,
-                                    ])
+      return StorageError.downloadSizeExceeded(
+        total: task.progress.totalUnitCount,
+        maxSize: maxSize
+      ) as NSError
     }
     return nil
   }
diff --git a/FirebaseStorage/Sources/StorageTaskSnapshot.swift b/FirebaseStorage/Sources/StorageTaskSnapshot.swift
index 3c909d67855..d7b291b06a8 100644
--- a/FirebaseStorage/Sources/StorageTaskSnapshot.swift
+++ b/FirebaseStorage/Sources/StorageTaskSnapshot.swift
@@ -68,7 +68,7 @@ import Foundation
        reference: StorageReference,
        progress: Progress,
        metadata: StorageMetadata? = nil,
-       error: NSError? = nil) {
+       error: Error? = nil) {
     self.task = task
     self.reference = reference
     self.progress = progress
diff --git a/FirebaseStorage/Sources/StorageUploadTask.swift b/FirebaseStorage/Sources/StorageUploadTask.swift
index 8c99e8c4a28..5878b03fb04 100644
--- a/FirebaseStorage/Sources/StorageUploadTask.swift
+++ b/FirebaseStorage/Sources/StorageUploadTask.swift
@@ -237,14 +237,10 @@ import Foundation
        isFile == true {
       return nil
     }
-    let userInfo = [NSLocalizedDescriptionKey:
-      "File at URL: \(fileURL?.absoluteString ?? "") is not reachable."
-      + " Ensure file URL is not a directory, symbolic link, or invalid url."]
-    return NSError(
-      domain: StorageErrorDomain,
-      code: StorageErrorCode.unknown.rawValue,
-      userInfo: userInfo
-    )
+    return StorageError.unknown(message: "File at URL: \(fileURL?.absoluteString ?? "") is " +
+      "not reachable. Ensure file URL is not " +
+      "a directory, symbolic link, or invalid url.",
+      serverError: [:]) as NSError
   }
 
   func finishTaskWithStatus(status: StorageTaskStatus, snapshot: StorageTaskSnapshot) {
diff --git a/FirebaseStorage/Tests/Integration/StorageAsyncAwait.swift b/FirebaseStorage/Tests/Integration/StorageAsyncAwait.swift
index f8729a5b675..fa780f24a6c 100644
--- a/FirebaseStorage/Tests/Integration/StorageAsyncAwait.swift
+++ b/FirebaseStorage/Tests/Integration/StorageAsyncAwait.swift
@@ -118,11 +118,29 @@ class StorageAsyncAwait: StorageIntegrationCommon {
     do {
       _ = try await ref.putDataAsync(data)
       XCTFail("Unexpected success from unauthorized putData")
-    } catch let StorageError.unauthorized(bucket, object) {
+    } catch let StorageError.unauthorized(bucket, object, _) {
       XCTAssertEqual(bucket, "ios-opensource-samples.appspot.com")
       XCTAssertEqual(object, objectLocation)
     } catch {
-      XCTFail("error failed to convert to StorageError.unauthorized")
+      XCTFail("error failed to convert to StorageError.unauthorized  \(error)")
+    }
+  }
+
+  func testSimplePutDataUnauthorizedWithNSError() async throws {
+    let objectLocation = "ios/private/secretfile.txt"
+    let ref = storage.reference(withPath: objectLocation)
+    let data = try XCTUnwrap("Hello Swift World".data(using: .utf8), "Data construction failed")
+    do {
+      _ = try await ref.putDataAsync(data)
+      XCTFail("Unexpected success from unauthorized putData")
+    } catch {
+      let e = error as NSError
+      XCTAssertEqual(e.domain, StorageErrorDomain)
+      XCTAssertEqual(e.code, StorageErrorCode.unauthorized.rawValue)
+      XCTAssertEqual(e.localizedDescription, "User does not have permission to access" +
+        " gs://ios-opensource-samples.appspot.com/ios/private/secretfile.txt.")
+      XCTAssertEqual(e.userInfo["ResponseErrorCode"] as? Int, 403)
+      print(e)
     }
   }
 
@@ -136,13 +154,13 @@ class StorageAsyncAwait: StorageIntegrationCommon {
     do {
       _ = try await ref.putFileAsync(from: fileURL)
       XCTFail("Unexpected success from putFile of a directory")
-    } catch let StorageError.unknown(reason) {
+    } catch let StorageError.unknown(reason, _) {
       XCTAssertTrue(reason.starts(with: "File at URL:"))
       XCTAssertTrue(reason.hasSuffix(
         "is not reachable. Ensure file URL is not a directory, symbolic link, or invalid url."
       ))
     } catch {
-      XCTFail("error failed to convert to StorageError.unknown")
+      XCTFail("error failed to convert to StorageError.unknown \(error)")
     }
   }
 
diff --git a/FirebaseStorage/Tests/Integration/StorageIntegration.swift b/FirebaseStorage/Tests/Integration/StorageIntegration.swift
index 2f407dc7427..7e93e84e7fe 100644
--- a/FirebaseStorage/Tests/Integration/StorageIntegration.swift
+++ b/FirebaseStorage/Tests/Integration/StorageIntegration.swift
@@ -201,7 +201,7 @@ class StorageResultTests: StorageIntegrationCommon {
         XCTFail("Unexpected success from unauthorized putData")
       case let .failure(error as StorageError):
         switch error {
-        case let .unauthorized(bucket, object):
+        case let .unauthorized(bucket, object, serverError):
           XCTAssertEqual(bucket, "ios-opensource-samples.appspot.com")
           XCTAssertEqual(object, file)
           expectation.fulfill()
diff --git a/FirebaseStorage/Tests/Unit/StorageAPITests.swift b/FirebaseStorage/Tests/Unit/StorageAPITests.swift
index 02d6b06e557..0244d22f7be 100644
--- a/FirebaseStorage/Tests/Unit/StorageAPITests.swift
+++ b/FirebaseStorage/Tests/Unit/StorageAPITests.swift
@@ -158,6 +158,9 @@ final class StorageAPITests: XCTestCase {
     case .downloadSizeExceeded: return code
     case .cancelled: return code
     case .invalidArgument: return code
+    case .bucketMismatch: return code
+    case .internalError: return code
+    case .pathError: return code
     @unknown default:
       fatalError()
     }
diff --git a/FirebaseStorage/Tests/Unit/StorageGetMetadataTests.swift b/FirebaseStorage/Tests/Unit/StorageGetMetadataTests.swift
index 52592be27c2..a711125d8c1 100644
--- a/FirebaseStorage/Tests/Unit/StorageGetMetadataTests.swift
+++ b/FirebaseStorage/Tests/Unit/StorageGetMetadataTests.swift
@@ -181,7 +181,9 @@ class StorageGetMetadataTests: StorageTestHelpers {
       fetcherService: fetcherService!.self,
       queue: dispatchQueue!.self
     ) { metadata, error in
-      XCTAssertEqual((error as? NSError)!.code, StorageErrorCode.unknown.rawValue)
+      XCTAssertNil(metadata)
+      let nsError = try! XCTUnwrap(error as? NSError)
+      XCTAssertEqual(nsError.code, StorageErrorCode.unknown.rawValue)
       expectation.fulfill()
     }
     task.enqueue()
diff --git a/FirebaseStorage/Tests/Unit/StorageReferenceTests.swift b/FirebaseStorage/Tests/Unit/StorageReferenceTests.swift
index 56efb5cabf9..7854bedcf15 100644
--- a/FirebaseStorage/Tests/Unit/StorageReferenceTests.swift
+++ b/FirebaseStorage/Tests/Unit/StorageReferenceTests.swift
@@ -72,7 +72,7 @@ class StorageReferenceTests: XCTestCase {
     XCTAssertThrowsError(try storage!.reference(for: url), "This was supposed to fail.") { error in
       XCTAssertEqual(
         "\(error)",
-        "bucketMismatch(\"Provided bucket: `bcket` does not match the Storage " +
+        "bucketMismatch(message: \"Provided bucket: `bcket` does not match the Storage " +
           "bucket of the current instance: `bucket`\")"
       )
     }
@@ -95,8 +95,9 @@ class StorageReferenceTests: XCTestCase {
   func testBadBucketScheme2() throws {
     let url = try XCTUnwrap(URL(string: "htttp://bucket/"))
     XCTAssertThrowsError(try storage!.reference(for: url), "This was supposed to fail.") { error in
-      XCTAssertEqual("\(error)", "pathError(\"Internal error: URL scheme must be one of gs://, " +
-        "http://, or https://\")")
+      XCTAssertEqual("\(error)",
+                     "pathError(message: \"Internal error: URL scheme must be one of gs://, " +
+                       "http://, or https://\")")
     }
   }
 
@@ -219,7 +220,7 @@ class StorageReferenceTests: XCTestCase {
         XCTFail("Unexpected success.", file: #file, line: #line)
       case let .failure(error):
         switch error {
-        case let StorageError.unknown(message):
+        case let StorageError.unknown(message, serverError):
           let expectedDescription = "File at URL: \(dummyFileURL.absoluteString) " +
             "is not reachable. Ensure file URL is not a directory, symbolic link, or invalid url."
           XCTAssertEqual(expectedDescription, message)
diff --git a/FirebaseStorage/Tests/Unit/StorageTests.swift b/FirebaseStorage/Tests/Unit/StorageTests.swift
index ff605798ef8..bbc19194c62 100644
--- a/FirebaseStorage/Tests/Unit/StorageTests.swift
+++ b/FirebaseStorage/Tests/Unit/StorageTests.swift
@@ -45,7 +45,7 @@ class StorageTests: XCTestCase {
     XCTAssertThrowsError(try storage.reference(for: url), "This was supposed to fail.") { error in
       XCTAssertEqual(
         "\(error)",
-        "bucketMismatch(\"Provided bucket: `benwu-test2.storage.firebase.com` does not match the " +
+        "bucketMismatch(message: \"Provided bucket: `benwu-test2.storage.firebase.com` does not match the " +
           "Storage bucket of the current instance: `benwu-test1.storage.firebase.com`\")"
       )
     }
@@ -126,7 +126,7 @@ class StorageTests: XCTestCase {
     XCTAssertThrowsError(try storage.reference(for: url), "This was supposed to fail.") { error in
       XCTAssertEqual(
         "\(error)",
-        "bucketMismatch(\"Provided bucket: `bucket` does not match the " +
+        "bucketMismatch(message: \"Provided bucket: `bucket` does not match the " +
           "Storage bucket of the current instance: `notMyBucket`\")"
       )
     }


Test patch: