Iced + Tokio Spawn: Is there a better way to handle errors

I am trying to learn how to better perform error handling. In an iced project in the new function I create a task that runs 2 async functions. They have to run in series which I think eliminates the ability to use of the join macro. The code works but I seem to be stuck error handling with match ok/err statements. Is there a better way to deal with the error handling? I should note that both async functions return a result. Any assistance will be appreciated.

fn new() -> (Self, Task<Message>) {

    let initial_state = Self {
        screen: Screen::LoadingScreen,
        start_date_90: "".to_string(),
        start_date_180: "".to_string(),
        start_date_365: "".to_string(),
        start_date_3650: "".to_string(),
    };

    let initial_task = Task::future(async {
        use iced::futures::TryFutureExt;
        let handle1 = tokio::spawn(  // Result<bool, join error> assigned to handle1
            insert_csv_to_sql()
                .unwrap_or_else(|error| {
                    println!("Error inserting CSV data: {}", error);
                    false
                })
        );

        let result1 = match handle1.await {
            Ok(val) => val,
            Err(e) => {
                println!("Join error: {}", e);
                false
            },
        };  

        let mut result2: Vec<String> = Vec::new();
        if result1 == true {
            let handle2 = tokio::spawn(
                get_start_dates()
                    .unwrap_or_else(|error| {
                        println!("Error getting start dates: {}", error);
                        Vec::new()
                    })
            );

            result2 = match handle2.await {
                Ok(val) => val,
                Err(e) => {
                    println!("Join error: {}", e);
                    Vec::new()
                },
            }; 
        } 
        Message::UpdateStartDates(result2)
    });

    (initial_state, initial_task)
}


you don't need to spawn a task just to immediately await the handle, you can just await the future.

plus, the join error usually indicates a panic somewhere in the task (or, you explicitly canceled the task by calling handle.abort(), which you didn't in this case), so I usually do not handle join errors, but instead unwrap it to propagate the panic.

you way of using a boolean flag to indicate the error condition is not very common in rust. use the Result or Option instead, which support plenty of common patterns to handle errors, such as the try operator (a.k.a. the question mark), if-let, let-else, etc, you can also use the combinator methods like and_then(), or_else to chain steps, or you can simply branch on one of the predicate methods like is_some(), is_ok().

you also get to use the convenient methods from TryFutureExt when you use Result.

note, there are also chaining methods on bool, such as bool::then_some(), but these are not used as much as Option or Result.

for example, here's one way to rewrite this piece of code:

// first, change the signature of `insert_csv_to_sql` to return an `Result`, instead of just `bool`
async fn insert_csv_to_sql() -> Result<(), ()> {
    //...
    Ok(())
}

// then it's easy to use `TryFutureExt` to rewrite the code like this:
use futures::TryFutureExt;
let dates = insert_csv_to_sql()
	.inspect_err(|err| println!("error inserting csv cata: {err}"))
	.and_then(|_| {
		get_start_dates().map_err(|err| println!("error getting start dates: {err}"))
	})
	.await
	.unwrap_or_default();
Message::UpdateStartDates(dates)

Thank you for the response. It taught me a lot. Below is my updated working code and function signatures. I was unable to get map_err to work so I used inspect_err. I like the use of unwrap_or_default. If the result is an error it returns the default value for the given type which in this case is a vector. What I have been unable to determine is what is the default value for type vector. Is it just an empty vector with no associated content type?

pub async fn insert_csv_to_sql() -> Result<(), SqlCSVChronoError> {
pub async fn get_start_dates() -> Result<Vec<String>, SqlCSVChronoError > {

let initial_task = Task::future(async {
    use iced::futures::TryFutureExt;
    let dates = insert_csv_to_sql()
        .inspect_err(|error| println!("Error inserting CSV data: {}", error))
            // checks if there is an error and if so executes println
            // passes result on unchanged
        .and_then(|_| {
            // if result is ok run function below
            // if not, return error value
            get_start_dates().inspect_err(|error| println!("Error getting start dates: {}", error))
        }) // end of and then
        .await
        .unwrap_or_default();  
          // unwraps result
          // returns OK value or if error, default value
    Message::UpdateStartDates(dates)
    }
); // end of let initial task        

in my example, I used Result<(), ()> as return type of insert_csv_to_sql(), which has a different Err variant type than the return type of get_start_dates(), so I need map_err(). in your code, both function returns Result with the same Err type, so map_err() is not applicable here, and your choice of inspect_err() is the correct one to use.

yes.

the Default trait can only be implemented for types that are able to construct with no arguments, hence the name "default".

for Vec, the only meaningful definition of a "default" constructor is to create an empty container, since it's the natural and intuitive semantic, but also because it's a generic type, it must be implementable for all possible T.

the documentation explicitly specifies this behavior:


if you prefer a more imperative style over this "functional" style (using chaining combinators to compose smaller futures/async functions into larger one), a common trick to reduce the depth of nested control flow in a mult-step procedure is to return early on failure conditions, for example:

let initial_task = Task::future(async {

	// step 1: `Ok` contains no data, use `if-let` to match `Err` case, because it's shorter
	if let Err(err) = insert_csv_to_sql().await {
		println!("Error inserting CSV data: {}", err);
		return Message::UpdateStartDates(vec![]);
	}

	// step 2: both `Ok` and `Err` cases contain data, use `match`; diverges in `Err` case
	let result2 = match get_start_dates().await {
		Ok(dates) => dates,
		Err(err) => {
			println!("Error getting start dates: {}", err);
			return Message::UpdateStartDates(vec![]);
		}
	};

	// possibly more steps follows
	//...

	// finally, if execution reaches here, all steps are successful
	Message::UpdateStartDates(result2, /* result3, result4, ... */)
});

alternatively, you may construct the "successful" result in the final step match clause directly:

// step 1 is the same, diverge on `Err`
if let Err(err) = insert_csv_to_sql().await {
	println!("Error inserting CSV data: {}", err);
	return Message::UpdateStartDates(vec![]);
}

// step 2 is the final step, no `let` binding, return the `match` expression directly
match get_start_dates().await {
	Ok(dates) => Message::UpdateStartDates(dates),
	Err(err) => {
		println!("Error getting start dates: {}", err);
		Message::UpdateStartDates(vec![])
	}
}

note, some people argue multiple exits/return paths in a function (or async block, in this case) is bad for readability, although personally I don't mind. it's more of a personal preference I guess.

another point I want to mention regarding error handling: this example prints a message and returns a "default" (or "sentinel") value of the same type on error cases. this is a perfectly fine decision for an application, but if this is a library, it is usually preferrable and more idiomatic to pass the information of the error back to the caller instead of making a decision (e.g. to print a message to the terminal) on the spot, this makes the code more composible.