Leave a rating/review
Notes: 12. Handle Errors
Errors happen all the time, especially when working with networks. There are two levels of errors to think about when working with URL requests: 1. Thrown errors from the functions themselves. 2. Non-successful HTTP status codes.
You’re currently, technically, handling both. Whether it’s by using try statements for functions that throw or actually checking that the response code is of value 200, you print debug statements to the console for both. This is not, however, the best way to handle errors.
First, you don’t want to have print statements in production builds of your app; it’s fine for you to debug while developing, but not for a release version. Second, while you do check for these errors, none of it bubbles up to end users. If something goes wrong while downloading the song, your users get no feedback or indication.
With the current implementation, what would happen? Would the progress view be hidden and the button title revert back to Download?
I don’t know, and you possibly don’t as well. That’s because we haven’t explicitly handled this scenario and this is what users might run into, specially when it comes to all the different types of network connections and speeds to be encountered.
Let’s add some better error-handling to the app. Start by opening the Starter project for this episode, which is just a continuation from where the last episode left off.
Open SongDownloader.swift and update the downloadSong method:
func downloadSong(at url: URL) async throws { // THIS IS NEW!
…
}
You add the throws keyword to indicate that this method can throw errors that will need to be handled.
You can bubble up the thrown errors from the asynchronous data function on URLSession, but you don’t really have any error to bubble up and throw if the status code is invalid. Add the following code inside SongDownloader:
class SongDownloader: ObservableObject {
// MARK: Song Download Error
enum SongDownloadError: Error {
case invalidResponse
}
…
}
This adds an enum of possible errors that can occur. The first error is for when there is an invalid response from the request.
Next, inside downloadSong, remove the question mark (?) from the try line that performs the download and remove the guard that’s no longer necessary.
let (downloadURL, response) = try await session.download(from: url)
Perfect. You no longer print to the console as the only error-handling mechanism if something goes wrong with the request, you bubble up the error thrown from the download method itself.
An alternative solution to this, could be to keep the guard statement and instead bubbling up the error, you throw your own custom error. Either solution is fine, so let’s move on.
Next up is the print statement for when the response’s status code is not 200. Replace the code inside the guard statement with the following:
throw SongDownloadError.invalidResponse
Pretty straightforward so far. Since there is no specific thrown error when checking the status code that can be bubbled up, you throw your custom invalidResponse error.
The work that gets done next in this method revolves around file manager and acquiring the app’s Documents directory. So add an additional case to your errors enum:
enum SongDownloadError: Error {
case documentDirectoryError // THIS IS NEW!
case invalidResponse
}
And then change the code in the next guard statement:
guard let documentsPath = fileManager.urls(for: .documentDirectory,
in: .userDomainMask).first
else {
throw SongDownloadError.documentDirectoryError // THIS IS NEW!
}
The last error that you have a print statement for when copying the downloaded song to permanent storage. Add one more error case to your enum:
enum SongDownloadError: Error {
case documentDirectoryError
case failedToStoreSong // THIS IS NEW!
case invalidResponse
}
And replace the print statement with the following:
do {
if fileManager.fileExists(atPath: destinationURL.path) {
try fileManager.removeItem(at: destinationURL)
}
try fileManager.copyItem(at: downloadURL, to: destinationURL)
} catch {
throw SongDownloadError.failedToStoreSong // THIS IS NEW!
}
All the work you needed in SongDownloader has been done, now the UI code needs to be updated as the downloadSong method can throw errors.
Open SongDetailView.swift First up, add a property that’ll control whether an alert is shown to the user if the download fails:
@MainActor @State private var showDownloadFailedAlert: Bool = false
It’s a State property as it’ll work with SwiftUI and marked with the MainActor attribute in order to ensure any state changes happen on the main thread.
Now you have to handle the error inside of downloadTapped. This is happening because the downloadSong method on SongDownloader can now throw errors, but they’re not being handled here. Update the code that calls downloadSong with the following:
do {
try await downloader.downloadSong(at: previewURL)
} catch let error {
print(error)
showDownloadFailedAlert = true
}
You add try when calling downloadSong and wrap everything inside a do-catch block. The print statement is just a fun way to visualize your thrown errors from downloadSong, but feel free to remove that code if you aren’t interested in seeing that print statement or before shipping your app.
Finally, update the view code to actually show the alert using your new property. Add this modifier to your button, just before the disabled modifier:
Button(action: {
Task {
await downloadTapped()
}
}) {
if isDownloading {
Text("Downloading...")
} else {
Text(downloader.downloadLocation == nil ? "Download" : "Listen")
}
}
// THIS IS NEW!!!!
.alert("Download Failed", isPresented: $showDownloadFailedAlert) {
Button("Dismiss", role: .cancel) {
showDownloadFailedAlert = false
}
}
.disabled(isDownloading)
Build and run your app.
Tap Download, and check out the results. If the download went well, then you shouldn’t see any different behavior from what the app was dong in the previous episode.
If an error were to occur, however, then an alert will be shown in order to tell users that something went wrong.
It might be tricky to get an error to happen, but try disconnecting your computer or device from the internet and tapping the Download button or, alternatively, set your showDownloadFailedAlert to true at the end of the do block in order to manually trigger the alert. If you do, don’t forget to change the code back so you don’t end up shipping an inadvertent bug :)
Congrats. You’ve done very little changes to the actual look of your app, but behind-the-scenes you’re now handling errors in a much better way.
As you expand your downloadSong method of SongDownloader, or if you add more methods and functionality to SongDownloader itself, you’ve not only added an enum you can use to throw the same or new errors, but you’ve also established good error-handling patterns in your code!
Join me in the next episide, a challenge episode, where you’ll put some of your recently acquired knowledge to the test.
See ya there! :)