Lỗi có thể khôi phục với Result

Hầu hết các lỗi không nghiêm trọng lắm thường không yêu cầu chương trình chúng ta dừng lại.

Thỉnh thoảng, khi một hàm nào đó gặp lỗi vì một vài lý do nào đó mà bạn có thể dễ dàng giải thích và xử lý nó. Ví dụ, nếu bạn cố gắng mở một file và điều khiển đó bị lỗi bởi vì file không tồn tại, bạn có thể muốn tạo file thay vì kết thúc.

Nhắc lại từ “Handling Potential Failure with the Result Type” trong chương 2 mà Result enum là được định nghĩa như có 2 biến thể : Ok hoặc Err như sau:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

TE là các tham số với kiểu dữ liệu tổng quát (generic), chúng ta sẽ thảo luận về generic chi tiết hơn trong chương 10. Gì chúng ta biết về nó đến này là T thể hiện kiểu của dữ liệu sẽ đuợc trả về trong trường hợp thành công trong Ok, và E thể hiện kiểu của lỗi mà được trở về trong trường hợp lỗi với biến thể Err. Bởi vì Result có các tham số kiểu generic này, chúng ta có thể sử dụng kiểu Result và các hàm của nó định nghĩa trong nhiều trường hợp khác nhau nơi giá trị thành công hoặc giá trị lỗi có thể nhận các kiểu khác nhau.

Cùng xem xét việc gọi một hàm mà trở về một kiểu Result bởi vì hàm có thể lỗi. Trong Listing 9-3 chúng ta cố gắng mở một file

Filename: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");
}

Listing 9-3:Mở một file

Cách mà chúng ta biết File::open trở về một kiểu Result?. Chúng có thể xem standard library API documentation, hoặc chúng có thể yêu cầu trình biên dịch (compiler). Nếu chúng đưa f một ký annotation mà chúng ta biết là không phải là giá trị trở kiểu hàm và sau đó cố gắng để biên dịch, trình biên dịch sẽ nói với chúng ta thằng kiểu này sẽ không phù hợp (match). Thông điệpj lỗi sẽ nói với chunts ta kiểu giữ liệu nào f là phù hợp. Cùng thử ví dụ sau: Kiểu File::open không phải là u32, nếu thay đổi let f` tới

use std::fs::File;

fn main() {
    let f: u32 = File::open("hello.txt");
}

Cố gắng để biên dịch đưa chúng ta đầu ra như sau:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0308]: mismatched types
 --> src/main.rs:4:18
  |
4 |     let f: u32 = File::open("hello.txt");
  |            ---   ^^^^^^^^^^^^^^^^^^^^^^^ expected `u32`, found enum `Result`
  |            |
  |            expected due to this
  |
  = note: expected type `u32`
             found enum `Result<File, std::io::Error>`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `error-handling` due to previous error

Điều này nói với chúng ta kiểu trả về của hàm File::openResult<T,E> Tham số generic T đã được chỉ ra ở đây với kiểu giá trị thành công là std::fs::File, mà là một handle của 1 file (Dạng tham chiếu 1 file). Kiểu E được sử dụng trong giá trị lỗi là std::io::Error.

Kiểu giá trị trả về nghĩa là gọi File::open có thể thành công và trở về handle của file mà bạn có thể đọc hoặc ghi tới. Lời gọi hàm cũng có thể lỗi: Ví dụ, file có thể không tồn tại, hoặc chúng ta có thể không có quyền để truy xuất file. Hàm File::opne cần để có cách để nói với chúng ta liệu nó là thành công hoặc thất bại cùng lúc đưa chúng ta hoặc handle của file hoặc thông tin lỗi. Thông tin này chính là những gì Result enum truyền tải.

Trong trường hợp File::open thành công, giá trị trong biến f sẽ là thực thể của Ok mà bao gồm một file handle. Trong trường hợp lỗi, giá trị f là một thực thể Err mà bao gồm nhiều thông tin về kiểu lỗi đã xảy ra.

Chúng ta có thể thêm code trong Listing 9-3 để xử lý kiểu File::open trả về. Trong Listing 9-4 chỉ ra một cách để xử lý Result cơ bản, cú pháp match chúng ta đã thảo luận trong Chương 6.

Filename: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => panic!("Problem opening the file: {:?}", error),
    };
}

Listing 9-4: Sử dụng match để xử lý Result

Chú ý rằng, giống Option enum, kiểu Result enum và biến thể của nó có thể khai báo khi import, do đó chúng ta không cần chi tiết Result:: trước OkErrtrong cú pháp match.

Khi kết quả là Ok, code sẽ trở giá trị filevà sau đó chúng gán file handle tới biến f. Sau hàm match, chúng ta có thể sử dụng file handle cho việc đọc hoặc ghi.

Một nhánh khác của match xử lý trường hợp chúng ta nhận một giá trị Err từ File::open. Trong trường hợp này, chúng ta gọi macro panic!. Nếu không có file nào tên hello.txt trong đường dẫn hiện tại. Nếu chúng ta chạy code sau , chúng ta sẽ nhìn thấy đầu ra như sau nơi mà macro panic! được gọi

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/error-handling`
thread 'main' panicked at 'Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:8:23
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Thông thường, đàu ra nói với chúng ta chính xác lỗi gì.

Xử lý các lỗi khác nhau

Đoạn code trong Listing 9-4 sẽ panic! không quan trọng là File::open lỗi gì. Tuy nhiên, nếu chúng ta muốn xử lý khác nhau cho mỗi kiểu lý do lỗi. Nếu File::opem lỗi bởi vì file không tồn tại, chúng ta muốn tạo file và trở về một handle của một file mới. Nếu File::open lỗi cho bất kỳ lý do nào khác, ví như chúng ta không có quyền để mở một file chúng ta vẫn muốn code panic như chúng ta đã làm trong Listing 9-4. Ví dụ 9-5 sau thêm nhánh của match:

Filename: src/main.rs

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt");

    let f = match f {
        Ok(file) => file,
        Err(error) => match error.kind() {
            ErrorKind::NotFound => match File::create("hello.txt") {
                Ok(fc) => fc,
                Err(e) => panic!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error)
            }
        },
    };
}

Listing 9-5: Handling different kinds of errors in different ways

Kiểu của giá trị trả về File::open trong Errio::Error, nới là một cấu được cung cấp bởi thư viện chuẩn. Cấu trúc này có một phương thức kind mà chúng ta có thể gọi để nhận giá trị io::ErrorKind.

Enum io::ErrorKind được cung cấp bởi thư viện chuẩn và có biến thể thể hiện kiểu khác nhau của lỗi mà có thể trả về từ điều khiển io. BIến thể chúng ta muốn sử dụng ở đây là ErrorKind::NotFound, mà chỉ file chúng ta đang cố gắng để mở nhưng không tồn tại. Do đó Chúng ta, xử lý f, nhúng chúng cũng có kiểu trong error.kind()

Điều kiện chúng ta muốn kiểm tra trong cú pháp match là liệu giá trị trở bởi error.kind()NotFound - một biến thể của ErrorKind enum. Nếu đúng như vậy, chúng ta cố để tạo một file với File::create. Tuy nhiên, bởi vì File::create cũng có thể lỗi, chúng cần nhánh kiểm tra thứ 2 của match. Khi file không được tạo, một thông điệp lỗi khác sẽ được in ra. Nhánh thứ 2 của match sẽ được gọi nếu bất kỳ lỗi bên lỗi mising file (không tìm thấy file).

Thay đổi sử dụng cú pháp match với Result<T, E>

Biểu thức match khá không những hữu dụng mà còn khá là primitive (dạng nguyên bản). Trong chương 13, bạn sẽ học về closures, nơi được sử dụng với nhiều phương thức được định nghĩa trong Result<T,E>. Những phướng thức này có thể ngắn gọn hơn sử dụng match khi xử lý giá trị Result<T, E> trong code của bạn 9-5 but using closures and the unwrap_or_else method: Eg, đây là một cách khác để viết cùng một logic như vị dụ trong Listing 9-5 nhưng sử dụng closure và phương thức unwrap_or_else :

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let f = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

Mặc dù đoạn code này có cùng hành vi như Listing 9-5, nó không bao gồm biểu thức match và dễ dàng để đọc hơn. Quay lại ví dụ này sau khi bạn đã học Chương 13, và tìm kiếm phương thức unwrap_or_else trong tài liệu thư viện chuẩn. Nhiều phương thức khác có thể được sử dụng với match bên trong khi bạn xử lý với lỗi.

Xử lý panic của lỗi với: unwrap and expect

Sử dụng match cho các trường hợp này khá tốt, nhưng nó có thể dài dòng và không phải lúc nào hiểu ý định nó truyền tải. Kiểu Result<T,E> có nhiều phương thức trợ giúp được định nghĩa để làm cho nhiều dạng mục đích khác nhau. Phương thức unwrap là một phương thức thực hiện cú pháp dạng match chúng ta đã viết trong Listing 9-4. Nếu giá trị ResultOk, unwrap sẽ trở về gía trị nằm trong Ok. Nếu Result là kiểu Err, unwrap sẽ gọi macro panic! cho chúng ta. Đây là một ví dụ sử dụng unwrap

Filename: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").unwrap();
}

Nếu chúng ta chạy đoạn code này ngoài một file hello.txt, chúng ta sẽ nhìn thấy một thông điệp lỗi từ panic! được gọi do phương thức unwrap thực hiện.

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error {
repr: Os { code: 2, message: "No such file or directory" } }',
src/libcore/result.rs:906:4

Một cách tương tự, phương thức expect để chúng ta lựa chọn thông điệp cho panic!. Sử dụng expect thay unwrap và cung cấp một thông điệp lỗi rõ ràng có thể truyền tải ý định và làm cho dễ dò lỗi đến từ đâu. Cú pháp cho expect như sau:

Filename: src/main.rs

use std::fs::File;

fn main() {
    let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

Chúng ta sử dụng expect trong cùng một cách với unwrap: trở về một file handle hoặc gọi macro panic!. Thông điệp lỗi được sử dụng bởi expect trong lời gọi hàm tới panic! sẽ là tham số truyền vào expect, thay thế thông điệp panic! mặc định mà unwrap sử dụng.

thread 'main' panicked at 'Failed to open hello.txt: Error { repr: Os { code:
2, message: "No such file or directory" } }', src/libcore/result.rs:906:4

Bởi vì thông điệp lỗi này bắt đầu đoạn text mà chúng ta truyền vào Failed to open hello.txt, nó sẽ dễ dàng để tìm kiếm nơi thông điệp lỗi in ra. Nếu chúng ta sử dụng unwrap trong nhiều nơi, sẽ tốn nhiều thời gian để xác định chính xác unwrap nào gây ra lỗi panic bởi tất cả lời gọi unwrap in ra cùng một thông điệp giống nhau.

Lan Truyền Lỗi - Propagating Errors

Khi thực hiện của một hàm gọi một điều gì đó mà có lỗi, thay vì xử lý lỗi trong hàm này, bạn có thể trả về lỗi tới hàm gọi để hàm này có thể quyết định nên làm gì. Đó được gọi là propagating (lan truyền) lỗi và đưa nhiều điều khiển tới lời gọi hàm nơi có thể có nhiều thông tin hoặc logic để ra lệnh lỗi nên được xử lý hơn là gì trong ngữ cảnh code hiện tại.

Ví du, Listing 9-6 chỉ ra một hàm mà đọc một username từ một file. Nếu như file không tồn tài hoặc không thể đọc, hàm này sẽ trở về những lỗi này tới code mà gọi hàm.

Filename: src/main.rs

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}
}

Listing 9-6: Một hàm mà trả về lỗi tới nơi gọi nó sử dụng match

Hàm này có thể được viết trong nhiều cách ngắn gọn hơn, nhưng chúng ta hãy cứ bắt đầu làm vậy để có thể khám phá cách xử lý lỗi; cuối cùng, chúng ta sẽ chỉ cách làm nó ngắn gọn. Hãy nhìn và kiểu trả về của hàm: Result<String, io::Error>. Điều này nghĩa rằng hàm là đang trả về giá trị của kiểu Result<T, E>, nơi mà TString và kiểu tổng quát Eio::Error. Nếu hàm này thành công ngoại trừ bất kỳ lỗi nào, đoạn code mà gọi hàm này sẽ nhận giá trị Ok mà giữ giá trị String - một username mà hàm này đọc được. Nếu hàm này chạm trán lỗi bất kỳ, lời gọi hàm sẽ nhậ giá trị Err mà giữ một instance của io::Error mà bao gồm nhiều thông tin về lỗi gì. Chúng ta chọn io::Error như kiểu trả về của hàm này bởi vì kiểu xảy ra với điều khiển File::open với điều khiển gọi hàm này read_to_string cùng trả về kiểu này trong khi lỗi.

Thân hàm bắt đầu bởi gọi File::open. Sau đó, chúng ta xử lý Result với cú pháp match tương tự với ví dụ Listing 9-4. Nếu File::open thành công, file handle lưu giữ trong biến f và hàm tiếp tục chạy. Trong trường hợp Err lỗi, thay vì gọi panic!, chúng ta sử dụng từ khóa return để thoát khỏi hàm sớm và trả lỗi từ File::open ra ngoài,, nơi mà đoạn code gọi hàm của chúng ta.

Do đó, nếu chúng ta có file handle trong f, hàm sau đó sẽ tạo một String mới trong biến s và gọi tới phương thức read_to_string từ f để đọc nội dùng của file vào s. Phương thức read_to_string cũng trả về Result bởi vì nó có thể lỗi, mặc dù File::open thành công. Do đó chúng ta cần match khác để xử lý Result: Nếu read_to_string thành công, hàm của chúng đã thành công, và chúng ta trả về username từ một file được bọc trong Ok enum. Nếu read_to_string lỗi, chúng ta trả về giá trị lỗi trong cách giống chúng ta trả về giá trị lỗi khi xử lý File::open. Tuy nhiên, chúng ta không cần viết chính xác return, bởi đó là biểu thức cuối cùng của hàm rồi.

Đoạn code mà gọi hàm này sẽ xử lý hoặc Ok mà bao gồm username, hoặc là Err mà bao gồm io::Error. Nó phụ thuộc đoạn code gọi hàm quyết định sẽ làm gì với các giá trị này. Nếu đoạn code nhận một giá trị Err, nó có thể gọi panic! và crash chương trình, sử dụng username mặc định, hoặc tìm kiềm username trong một file khác chẳng hạn. Chúng ta không đủ thông tin về đoạn code gọi là thực sự đang cố gắng làm, do đó chúng lan truyền tất cả thông tin thành hoặc lỗi tới chúng để xử lý một cách chính xác.

Mẫu cho lan truyền lỗi là khá thông dụng trong Rust và Rust cũng cung cấp một toán tử ? để làm điều này dễ dàng hơn

Cú pháp làn truyền lỗi : Toán tử ?

Listing 9-7 là thực hiện của hàm read_username_from_file mà có cùng chức năng với Listing 9-6, nhưng nó sử dụng toán tử ?

Filename: src/main.rs

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut f = File::open("hello.txt")?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
}

Listing 9-7: Một hàm trả về lỗi tới lời gọi hàm sử dụng toán tử ?

? được đặc sau giá trị Result được định nghĩa để làm hầu như giống cách trong match mà chúng ta đã xử lý giá trị Result trong Listing 9-6. Nếu giá trị ResultOk, giá trị trong Ok sẽ trả về từ biểu thức này và chương trình sẽ tiếp tục. Nếu giá trị trả về là Err, Err sẽ trả về khỏi hàm giống như đã sử dụng từ khóa return,

Có một sử khác nhau giữa match từ ví dụ Listing 9-6 làm và toán tử ? thực hiện: giá trị lỗi mà ? đã gọi chạy qua hàm from, được định nghĩa trong trait From trong thư viện chuẩn, mà sử dụng để biến đổi lỗi từ kiểu này sang kiểu khác. Khi toán tử ? gọi hàm from, kiểu lỗi nhận được là biến thành lỗi định trong kiểu dữ liệu trả về hàm của chúng ta. Điều này là hữu ích khi một hàm trở về một lỗi mà thể hiện tất cả cách có thể lỗi, ngay cả một phần của nó lỗi cho nhiều lý do nào đó. Miễn là có thực hiện impl From for ReturnError để định nghĩa sự biến đổi trong hàm from, toán tử ? lo việc gọi hàm from một cách tự động cho bạn.

Trong ngữ cảnh Listing 9-7, ? gọi tại cuối File::open sẽ trở về giá trị trong Ok tới biến f. Nếu một lỗi xảy ra, toán tử ? sẽ trả trả về sớm và đưa Err tới lời gọi hàm. Hành vi giống nhau áp dụng tới ? tại nơi gọi hàm read_to_string.

Toán tử ? vứt bỏ nhiều thủ tục (boilerplate) và làm cho thực hiện hàm trở nên đơn giản. Chúng thậm chí có thể gọi liên tiếp ? cho đoạn code ngắn gọn hơn như chỉ ra trong Listing 9-8.

Filename: src/main.rs

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let mut s = String::new();

    File::open("hello.txt")?.read_to_string(&mut s)?;

    Ok(s)
}
}

Listing 9-8: Chaining method calls after the ? operator

Chúng ta tạo một String mới tới s như bắt đầu hàm. Thay vì tạo biến f, chúng ta chain (nối) lời gọi tới read_to_string trực tiếp kết quả của File::open("hello.txt")?. Chúng ta vẫn có ? tại cuối của read_to_string, và chúng ta vẫn trả về giá trị Ok`` bao gồm username trong s khi cả File::openread_to_string thành công. Hàm này làm cùng cách với Listing 9-=6 và Listing 9-7; nhưng viết theo một cách khá hữu hiệu.

Listing 9-9 chỉ ra một cách để làm ngắn gọn hàm fs::read_to_string.

Filename: src/main.rs

#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}
}

Listing 9-9: Using fs::read_to_string instead of opening and then reading the file

Reading a file into a string is a fairly common operation, so the standard library provides the convenient fs::read_to_string function that opens the file, creates a new String, reads the contents of the file, puts the contents into that String, and returns it. Of course, using fs::read_to_string doesn’t give us the opportunity to explain all the error handling, so we did it the longer way first.

Where The ? Operator Can Be Used

The ? operator can only be used in functions whose return type is compatible with the value the ? is used on. This is because the ? operator is defined to perform an early return of a value out of the function, in the same manner as the match expression we defined in Listing 9-6. In Listing 9-6, the match was using a Result value, and the early return arm returned an Err(e) value. The return type of the function has to be a Result so that it’s compatible with this return.

In Listing 9-10, let’s look at the error we’ll get if we use the ? operator in a main function with a return type incompatible with the type of the value we use ? on:

use std::fs::File;

fn main() {
    let f = File::open("hello.txt")?;
}

Listing 9-10: Attempting to use the ? in the main function that returns () won’t compile

This code opens a file, which might fail. The ? operator follows the Result value returned by File::open, but this main function has the return type of (), not Result. When we compile this code, we get the following error message:

$ cargo run
   Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
 --> src/main.rs:4:36
  |
3 | / fn main() {
4 | |     let f = File::open("hello.txt")?;
  | |                                    ^ cannot use the `?` operator in a function that returns `()`
5 | | }
  | |_- this function should return `Result` or `Option` to accept `?`
  |
  = help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` due to previous error

This error points out that we’re only allowed to use the ? operator in a function that returns Result, Option, or another type that implements FromResidual. To fix the error, you have two choices. One choice is to change the return type of your function to be compatible with the value you’re using the ? operator on as long as you have no restrictions preventing that. The other technique is to use a match or one of the Result<T, E> methods to handle the Result<T, E> in whatever way is appropriate.

The error message also mentioned that ? can be used with Option<T> values as well. As with using ? on Result, you can only use ? on Option in a function that returns an Option. The behavior of the ? operator when called on an Option<T> is similar to its behavior when called on a Result<T, E>: if the value is None, the None will be returned early from the function at that point. If the value is Some, the value inside the Some is the resulting value of the expression and the function continues. Listing 9-11 has an example of a function that finds the last character of the first line in the given text:

fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

fn main() {
    assert_eq!(
        last_char_of_first_line("Hello, world\nHow are you today?"),
        Some('d')
    );

    assert_eq!(last_char_of_first_line(""), None);
    assert_eq!(last_char_of_first_line("\nhi"), None);
}

Listing 9-11: Using the ? operator on an Option<T> value

This function returns Option<char> because it’s possible that there is a character there, but it’s also possible that there isn’t. This code takes the text string slice argument and calls the lines method on it, which returns an iterator over the lines in the string. Because this function wants to examine the first line, it calls next on the iterator to get the first value from the iterator. If text is the empty string, this call to next will return None, in which case we use ? to stop and return None from last_char_of_first_line. If text is not the empty string, next will return a Some value containing a string slice of the first line in text.

The ? extracts the string slice, and we can call chars on that string slice to get an iterator of its characters. We’re interested in the last character in this first line, so we call last to return the last item in the iterator. This is an Option because it’s possible that the first line is the empty string, for example if text starts with a blank line but has characters on other lines, as in "\nhi". However, if there is a last character on the first line, it will be returned in the Some variant. The ? operator in the middle gives us a concise way to express this logic, allowing us to implement the function in one line. If we couldn’t use the ? operator on Option, we’d have to implement this logic using more method calls or a match expression.

Note that you can use the ? operator on a Result in a function that returns Result, and you can use the ? operator on an Option in a function that returns Option, but you can’t mix and match. The ? operator won’t automatically convert a Result to an Option or vice versa; in those cases, you can use methods like the ok method on Result or the ok_or method on Option to do the conversion explicitly.

So far, all the main functions we’ve used return (). The main function is special because it’s the entry and exit point of executable programs, and there are restrictions on what its return type can be for the programs to behave as expected.

Luckily, main can also return a Result<(), E>. Listing 9-12 has the code from Listing 9-10 but we’ve changed the return type of main to be Result<(), Box<dyn Error>> and added a return value Ok(()) to the end. This code will now compile:

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let f = File::open("hello.txt")?;

    Ok(())
}

Listing 9-12: Changing main to return Result<(), E> allows the use of the ? operator on Result values

The Box<dyn Error> type is a trait object, which we’ll talk about in the “Using Trait Objects that Allow for Values of Different Types” section in Chapter 17. For now, you can read Box<dyn Error> to mean “any kind of error.” Using ? on a Result value in a main function with the error type Box<dyn Error> is allowed, because it allows any Err value to be returned early.

When a main function returns a Result<(), E>, the executable will exit with a value of 0 if main returns Ok(()) and will exit with a nonzero value if main returns an Err value. Executables written in C return integers when they exit: programs that exit successfully return the integer 0, and programs that error return some integer other than 0. Rust also returns integers from executables to be compatible with this convention.

The main function may return any types that implement the std::process::Termination trait. As of this writing, the Termination trait is an unstable feature only available in Nightly Rust, so you can’t yet implement it for your own types in Stable Rust, but you might be able to someday!

Now that we’ve discussed the details of calling panic! or returning Result, let’s return to the topic of how to decide which is appropriate to use in which cases.