Skip to content

Commit e647348

Browse files
committed
Merge remote-tracking branch 'aturon/error-chaining'
2 parents de9d34a + 103ede8 commit e647348

File tree

1 file changed

+347
-0
lines changed

1 file changed

+347
-0
lines changed

active/0000-error-chaining.md

Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
- Start Date: (fill me in with today's date, 2014-07-17)
2+
- RFC PR #: (leave this empty)
3+
- Rust Issue #: (leave this empty)
4+
5+
# Summary
6+
7+
This RFC improves interoperation between APIs with different error
8+
types. It proposes to:
9+
10+
* Increase the flexibility of the `try!` macro for clients of multiple
11+
libraries with disparate error types.
12+
13+
* Standardize on basic functionality that any error type should have
14+
by introducing an `Error` trait.
15+
16+
* Support easy error chaining when crossing abstraction boundaries.
17+
18+
The proposed changes are all library changes; no language changes are
19+
needed -- except that this proposal depends on
20+
[multidispatch](https://github.com/rust-lang/rfcs/pull/195) happening.
21+
22+
# Motivation
23+
24+
Typically, a module (or crate) will define a custom error type encompassing the
25+
possible error outcomes for the operations it provides, along with a custom
26+
`Result` instance baking in this type. For example, we have `io::IoError` and
27+
`io::IoResult<T> = Result<T, io::IoError>`, and similarly for other libraries.
28+
Together with the `try!` macro, the story for interacting with errors for a
29+
single library is reasonably good.
30+
31+
However, we lack infrastructure when consuming or building on errors from
32+
multiple APIs, or abstracting over errors.
33+
34+
## Consuming multiple error types
35+
36+
Our current infrastructure for error handling does not cope well with
37+
mixed notions of error.
38+
39+
Abstractly, as described by
40+
[this issue](https://github.com/rust-lang/rust/issues/14419), we
41+
cannot do the following:
42+
43+
```
44+
fn func() -> Result<T, Error> {
45+
try!(may_return_error_type_A());
46+
try!(may_return_error_type_B());
47+
}
48+
```
49+
50+
Concretely, imagine a CLI application that interacts both with files
51+
and HTTP servers, using `std::io` and an imaginary `http` crate:
52+
53+
```
54+
fn download() -> Result<(), CLIError> {
55+
let contents = try!(http::get(some_url));
56+
let file = try!(File::create(some_path));
57+
try!(file.write_str(contents));
58+
Ok(())
59+
}
60+
```
61+
62+
The `download` function can encounter both `io` and `http` errors, and
63+
wants to report them both under the common notion of `CLIError`. But
64+
the `try!` macro only works for a single error type at a time.
65+
66+
There are roughly two scenarios where multiple library error types
67+
need to be coalesced into a common type, each with different needs:
68+
application error reporting, and library error reporting
69+
70+
### Application error reporting: presenting errors to a user
71+
72+
An application is generally the "last stop" for error handling: it's
73+
the point at which remaining errors are presented to the user in some
74+
form, when they cannot be handled programmatically.
75+
76+
As such, the data needed for application-level errors is usually
77+
related to human interaction. For a CLI application, a short text
78+
description and longer verbose description are usually all that's
79+
needed. For GUI applications, richer data is sometimes required, but
80+
usually not a full `enum` describing the full range of errors.
81+
82+
Concretely, then, for something like the `download` function above,
83+
for a CLI application, one might want `CLIError` to roughly be:
84+
85+
```rust
86+
struct CLIError<'a> {
87+
description: &'a str,
88+
detail: Option<String>,
89+
... // possibly more fields here; see detailed design
90+
}
91+
```
92+
93+
Ideally, one could use the `try!` macro as in the `download` example
94+
to coalesce a variety of error types into this single, simple
95+
`struct`.
96+
97+
### Library error reporting: abstraction boundaries
98+
99+
When one library builds on others, it needs to translate from their
100+
error types to its own. For example, a web server framework may build
101+
on a library for accessing a SQL database, and needs some way to
102+
"lift" SQL errors to its own notion of error.
103+
104+
In general, a library may not want to reveal the upstream libraries it
105+
relies on -- these are implementation details which may change over
106+
time. Thus, it is critical that the error type of upstream libraries
107+
not leak, and "lifting" an error from one library to another is a way
108+
of imposing an abstraction boundaries.
109+
110+
In some cases, the right way to lift a given error will depend on the
111+
operation and context. In other cases, though, there will be a general
112+
way to embed one kind of error in another (usually via a
113+
["cause chain"](http://docs.oracle.com/javase/tutorial/essential/exceptions/chained.html)). Both
114+
scenarios should be supported by Rust's error handling infrastructure.
115+
116+
## Abstracting over errors
117+
118+
Finally, libraries sometimes need to work with errors in a generic
119+
way. For example, the `serialize::Encoder` type takes is generic over
120+
an arbitrary error type `E`. At the moment, such types are completely
121+
arbitrary: there is no `Error` trait giving common functionality
122+
expected of all errors. Consequently, error-generic code cannot
123+
meaningfully interact with errors.
124+
125+
(See [this issue](https://github.com/rust-lang/rust/issues/15036) for
126+
a concrete case where a bound would be useful; note, however, that the
127+
design below does not cover this use-case, as explained in
128+
Alternatives.)
129+
130+
Languages that provide exceptions often have standard exception
131+
classes or interfaces that guarantee some basic functionality,
132+
including short and detailed descriptions and "causes". We should
133+
begin developing similar functionality in `libstd` to ensure that we
134+
have an agreed-upon baseline error API.
135+
136+
# Detailed design
137+
138+
We can address all of the problems laid out in the Motivation section
139+
by adding some simple library code to `libstd`, so this RFC will
140+
actually give a full implementation.
141+
142+
**Note**, however, that this implementation relies on the
143+
[multidispatch](https://github.com/rust-lang/rfcs/pull/195) proposal
144+
currently under consideration.
145+
146+
The proposal consists of two pieces: a standardized `Error` trait and
147+
extensions to the `try!` macro.
148+
149+
## The `Error` trait
150+
151+
The standard `Error` trait follows very the widespread pattern found
152+
in `Exception` base classes in many languages:
153+
154+
```rust
155+
pub trait Error: Send + Any {
156+
fn description(&self) -> &str;
157+
158+
fn detail(&self) -> Option<&str> { None }
159+
fn cause(&self) -> Option<&Error> { None }
160+
}
161+
```
162+
163+
Every concrete error type should provide at least a description. By
164+
making this a slice-returning method, it is possible to define
165+
lightweight `enum` error types and then implement this method as
166+
returning static string slices depending on the variant.
167+
168+
The `cause` method allows for cause-chaining when an error crosses
169+
abstraction boundaries. The cause is recorded as a trait object
170+
implementing `Error`, which makes it possible to read off a kind of
171+
abstract backtrace (often more immediately helpful than a full
172+
backtrace).
173+
174+
It's worth comparing the `Error` trait to the most widespread error
175+
type in `libstd`, `IoError`:
176+
177+
```rust
178+
pub struct IoError {
179+
pub kind: IoErrorKind,
180+
pub desc: &'static str,
181+
pub detail: Option<String>,
182+
}
183+
```
184+
185+
Code that returns or asks for an `IoError` explicitly will be able to
186+
access the `kind` field and thus react differently to different kinds
187+
of errors. But code that works with a generic `Error` (e.g.,
188+
application code) sees only the human-consumable parts of the error.
189+
In particular, application code will often employ `Box<Error>` as the
190+
error type when reporting errors to the user. The `try!` macro
191+
support, explained below, makes doing so ergonomic.
192+
193+
## An extended `try!` macro
194+
195+
The other piece to the proposal is a way for `try!` to automatically
196+
convert between different types of errors.
197+
198+
The idea is to introduce a trait `FromError<E>` that says how to
199+
convert from some lower-level error type `E` to `Self`. The `try!`
200+
macro then passes the error it is given through this conversion before
201+
returning:
202+
203+
```rust
204+
// E here is an "input" for dispatch, so conversions from multiple error
205+
// types can be provided
206+
pub trait FromError<E> {
207+
fn from_err(err: E) -> Self;
208+
}
209+
210+
impl<E> FromError<E> for E {
211+
fn from_err(err: E) -> E {
212+
err
213+
}
214+
}
215+
216+
impl<E: Error> FromError<E> for Box<Error> {
217+
fn from_err(err: E) -> Box<Error> {
218+
box err as Box<Error>
219+
}
220+
}
221+
222+
macro_rules! try (
223+
($expr:expr) => ({
224+
use error;
225+
match $expr {
226+
Ok(val) => val,
227+
Err(err) => return Err(error::FromError::from_err(err))
228+
}
229+
})
230+
)
231+
```
232+
233+
This code depends on
234+
[multidispatch](https://github.com/rust-lang/rfcs/pull/195), because
235+
the conversion depends on both the source and target error types. (In
236+
today's Rust, the two implementations of `FromError` given above would
237+
be considered overlapping.)
238+
239+
Given the blanket `impl` of `FromError<E>` for `E`, all existing uses
240+
of `try!` would continue to work as-is.
241+
242+
With this infrastructure in place, application code can generally use
243+
`Box<Error>` as its error type, and `try!` will take care of the rest:
244+
245+
```
246+
fn download() -> Result<(), Box<Error>> {
247+
let contents = try!(http::get(some_url));
248+
let file = try!(File::create(some_path));
249+
try!(file.write_str(contents));
250+
Ok(())
251+
}
252+
```
253+
254+
Library code that defines its own error type can define custom
255+
`FromError` implementations for lifting lower-level errors (where the
256+
lifting should also perform cause chaining) -- at least when the
257+
lifting is uniform across the library. The effect is that the mapping
258+
from one error type into another only has to be written one, rather
259+
than at every use of `try!`:
260+
261+
```
262+
impl FromError<ErrorA> MyError { ... }
263+
impl FromError<ErrorB> MyError { ... }
264+
265+
fn my_lib_func() -> Result<T, MyError> {
266+
try!(may_return_error_type_A());
267+
try!(may_return_error_type_B());
268+
}
269+
```
270+
271+
# Drawbacks
272+
273+
The main drawback is that the `try!` macro is a bit more complicated.
274+
275+
# Unresolved questions
276+
277+
## Conventions
278+
279+
This RFC does not define any particular conventions around cause
280+
chaining or concrete error types. It will likely take some time and
281+
experience using the proposed infrastructure before we can settle
282+
these conventions.
283+
284+
## Extensions
285+
286+
The functionality in the `Error` trait is quite minimal, and should
287+
probably grow over time. Some additional functionality might include:
288+
289+
### Features on the `Error` trait
290+
291+
* **Generic creation of `Error`s.** It might be useful for the `Error`
292+
trait to expose an associated constructor. See
293+
[this issue](https://github.com/rust-lang/rust/issues/15036) for an
294+
example where this functionality would be useful.
295+
296+
* **Mutation of `Error`s**. The `Error` trait could be expanded to
297+
provide setters as well as getters.
298+
299+
The main reason not to include the above two features is so that
300+
`Error` can be used with extremely minimal data structures,
301+
e.g. simple `enum`s. For such data structures, it's possible to
302+
produce fixed descriptions, but not mutate descriptions or other error
303+
properties. Allowing generic creation of any `Error`-bounded type
304+
would also require these `enum`s to include something like a
305+
`GenericError` variant, which is unfortunate. So for now, the design
306+
sticks to the least common denominator.
307+
308+
### Concrete error types
309+
310+
On the other hand, for code that doesn't care about the footprint of
311+
its error types, it may be useful to provide something like the
312+
following generic error type:
313+
314+
```rust
315+
pub struct WrappedError<E> {
316+
pub kind: E,
317+
pub description: String,
318+
pub detail: Option<String>,
319+
pub cause: Option<Box<Error>>
320+
}
321+
322+
impl<E: Show> WrappedError<E> {
323+
pub fn new(err: E) {
324+
WrappedErr {
325+
kind: err,
326+
description: err.to_string(),
327+
detail: None,
328+
cause: None
329+
}
330+
}
331+
}
332+
333+
impl<E> Error for WrappedError<E> {
334+
fn description(&self) -> &str {
335+
self.description.as_slice()
336+
}
337+
fn detail(&self) -> Option<&str> {
338+
self.detail.as_ref().map(|s| s.as_slice())
339+
}
340+
fn cause(&self) -> Option<&Error> {
341+
self.cause.as_ref().map(|c| &**c)
342+
}
343+
}
344+
```
345+
346+
This type can easily be added later, so again this RFC sticks to the
347+
minimal functionality for now.

0 commit comments

Comments
 (0)