Skip to main content

typespec/error/
mod.rs

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT License.
3
4//! Interfaces for working with errors.
5
6#[cfg(feature = "http")]
7use crate::http::{RawResponse, StatusCode};
8use std::{
9    backtrace::{Backtrace, BacktraceStatus},
10    borrow::Cow,
11    fmt,
12};
13
14/// A convenience alias for `Result` where the error type is hard coded to [`Error`].
15pub type Result<T> = std::result::Result<T, Error>;
16
17/// The kind of error.
18///
19/// The classification of error is intentionally fairly coarse.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum ErrorKind {
22    /// An HTTP status code that was not expected.
23    #[cfg(feature = "http")]
24    HttpResponse {
25        /// An HTTP status code.
26        status: StatusCode,
27        /// An error code returned by the service, or a friendly description of the `status`.
28        error_code: Option<String>,
29        /// The raw response returned by the service.
30        raw_response: Option<Box<RawResponse>>,
31    },
32    /// A connection to the server could not be established.
33    ///
34    /// The request was never sent, so it is safe to retry both reads and writes.
35    Connection,
36    /// An error performing IO.
37    Io,
38    /// An error converting data.
39    DataConversion,
40    /// An error getting an API credential token.
41    Credential,
42    /// A catch all for other kinds of errors.
43    Other,
44}
45
46impl ErrorKind {
47    /// Consumes the `ErrorKind` and converts to an [`Error`].
48    pub fn into_error(self) -> Error {
49        Error {
50            context: Repr::Simple(self),
51            backtrace: capture_backtrace(),
52        }
53    }
54}
55
56impl fmt::Display for ErrorKind {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            #[cfg(feature = "http")]
60            ErrorKind::HttpResponse {
61                status, error_code, ..
62            } => f
63                .debug_tuple("HttpResponse")
64                .field(status)
65                .field(&error_code.as_deref().unwrap_or("(unknown error code)"))
66                .finish(),
67            ErrorKind::Connection => f.write_str("Connection"),
68            ErrorKind::Io => f.write_str("Io"),
69            ErrorKind::DataConversion => f.write_str("DataConversion"),
70            ErrorKind::Credential => f.write_str("Credential"),
71            ErrorKind::Other => f.write_str("Other"),
72        }
73    }
74}
75
76/// An error encountered when communicating with the service.
77pub struct Error {
78    context: Repr,
79    // Only `Some` when `RUST_BACKTRACE` is set; boxed so the `Some` variant
80    // doesn't inflate `Error` beyond `clippy::result_large_err` limits.
81    backtrace: Option<Box<Backtrace>>,
82}
83
84impl fmt::Debug for Error {
85    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
86        let mut dbg = f.debug_struct("Error");
87        dbg.field("context", &self.context);
88        if let Some(backtrace) = &self.backtrace {
89            return dbg.field("backtrace", backtrace).finish();
90        }
91        dbg.finish_non_exhaustive()
92    }
93}
94
95impl Error {
96    /// Create a new `Error` based on an [`ErrorKind`] and an underlying error cause.
97    pub fn new<E>(kind: ErrorKind, error: E) -> Self
98    where
99        E: Into<Box<dyn std::error::Error + Send + Sync>>,
100    {
101        Self {
102            context: Repr::Custom(Custom {
103                kind,
104                error: error.into(),
105            }),
106            backtrace: capture_backtrace(),
107        }
108    }
109
110    /// Create a new `Error` based on an [`ErrorKind`], an underlying error cause, and a message.
111    #[must_use]
112    pub fn with_error<E, C>(kind: ErrorKind, error: E, message: C) -> Self
113    where
114        E: Into<Box<dyn std::error::Error + Send + Sync>>,
115        C: Into<Cow<'static, str>>,
116    {
117        Self {
118            context: Repr::CustomMessage(
119                Custom {
120                    kind,
121                    error: error.into(),
122                },
123                message.into(),
124            ),
125            backtrace: capture_backtrace(),
126        }
127    }
128
129    /// Create a new `Error` based on an [`ErrorKind`], an underlying error cause, and a function that returns a message.
130    #[must_use]
131    pub fn with_error_fn<E, F, C>(kind: ErrorKind, error: E, f: F) -> Self
132    where
133        E: Into<Box<dyn std::error::Error + Send + Sync>>,
134        F: FnOnce() -> C,
135        C: Into<Cow<'static, str>>,
136    {
137        Self::with_error(kind, error, f())
138    }
139
140    /// Create an `Error` based on an [`ErrorKind`] and a message.
141    #[must_use]
142    pub fn with_message<C>(kind: ErrorKind, message: C) -> Self
143    where
144        C: Into<Cow<'static, str>>,
145    {
146        Self {
147            context: Repr::SimpleMessage(kind, message.into()),
148            backtrace: capture_backtrace(),
149        }
150    }
151
152    /// Creates an `Error` based on an [`ErrorKind`] and a function that returns a message.
153    #[must_use]
154    pub fn with_message_fn<F, C>(kind: ErrorKind, f: F) -> Self
155    where
156        F: FnOnce() -> C,
157        C: Into<Cow<'static, str>>,
158    {
159        Self::with_message(kind, f())
160    }
161
162    /// Wrap this error with an additional message.
163    #[must_use]
164    pub fn with_context<C>(self, message: C) -> Self
165    where
166        C: Into<Cow<'static, str>>,
167    {
168        Self::with_error(self.kind().clone(), self, message)
169    }
170
171    /// Wrap this error with an additional message returned from a function.
172    #[must_use]
173    pub fn with_context_fn<F, C>(self, f: F) -> Self
174    where
175        F: FnOnce() -> C,
176        C: Into<Cow<'static, str>>,
177    {
178        self.with_context(f())
179    }
180
181    /// Get the [`ErrorKind`] of this `Error`.
182    pub fn kind(&self) -> &ErrorKind {
183        match &self.context {
184            Repr::Simple(kind)
185            | Repr::SimpleMessage(kind, ..)
186            | Repr::Custom(Custom { kind, .. })
187            | Repr::CustomMessage(Custom { kind, .. }, _) => kind,
188        }
189    }
190
191    /// If this error is an HTTP response error, return the associated status code.
192    #[cfg(feature = "http")]
193    pub fn http_status(&self) -> Option<StatusCode> {
194        match &self.kind() {
195            ErrorKind::HttpResponse { status, .. } => Some(*status),
196            _ => None,
197        }
198    }
199
200    /// Consumes the `Error`, returning its inner error, if any.
201    pub fn into_inner(self) -> std::result::Result<Box<dyn std::error::Error + Send + Sync>, Self> {
202        match self.context {
203            Repr::Custom(Custom { error, .. }) | Repr::CustomMessage(Custom { error, .. }, _) => {
204                Ok(error)
205            }
206            _ => Err(self),
207        }
208    }
209
210    /// Consumes the error, attempting to downcast the inner error as the type provided.
211    ///
212    /// Returns `Err(self)` if the downcast is not possible.
213    pub fn into_downcast<T: std::error::Error + 'static>(self) -> std::result::Result<T, Self> {
214        if self.downcast_ref::<T>().is_none() {
215            return Err(self);
216        }
217        // Unwrapping is ok here since we already check above that the downcast will work
218        Ok(*self.into_inner()?.downcast().expect("downcast is Some(T)"))
219    }
220
221    /// Returns a reference to the inner error wrapped by this error, if any.
222    pub fn get_ref(&self) -> Option<&(dyn std::error::Error + Send + Sync + 'static)> {
223        match &self.context {
224            Repr::Custom(Custom { error, .. }) | Repr::CustomMessage(Custom { error, .. }, _) => {
225                Some(error.as_ref())
226            }
227            _ => None,
228        }
229    }
230
231    /// Returns a reference to the inner error, if any, downcast to the type provided.
232    pub fn downcast_ref<T: std::error::Error + 'static>(&self) -> Option<&T> {
233        self.get_ref()?.downcast_ref()
234    }
235
236    /// Returns a mutable reference to the inner error wrapped by this error, if any.
237    pub fn get_mut(&mut self) -> Option<&mut (dyn std::error::Error + Send + Sync + 'static)> {
238        match &mut self.context {
239            Repr::Custom(Custom { error, .. }) | Repr::CustomMessage(Custom { error, .. }, _) => {
240                Some(error.as_mut())
241            }
242            _ => None,
243        }
244    }
245
246    /// Returns a mutable reference to the inner error, if any, downcasting to the type provided.
247    pub fn downcast_mut<T: std::error::Error + 'static>(&mut self) -> Option<&mut T> {
248        self.get_mut()?.downcast_mut()
249    }
250}
251
252impl std::error::Error for Error {
253    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
254        match &self.context {
255            Repr::Custom(Custom { error, .. }) | Repr::CustomMessage(Custom { error, .. }, _) => {
256                Some(&**error)
257            }
258            _ => None,
259        }
260    }
261}
262
263impl From<ErrorKind> for Error {
264    fn from(kind: ErrorKind) -> Self {
265        Self {
266            context: Repr::Simple(kind),
267            backtrace: capture_backtrace(),
268        }
269    }
270}
271
272impl From<std::io::Error> for Error {
273    fn from(error: std::io::Error) -> Self {
274        Self::new(ErrorKind::Io, error)
275    }
276}
277
278impl From<std::str::ParseBoolError> for Error {
279    fn from(error: std::str::ParseBoolError) -> Self {
280        Self::new(ErrorKind::DataConversion, error)
281    }
282}
283
284impl From<std::num::ParseIntError> for Error {
285    fn from(error: std::num::ParseIntError) -> Self {
286        Self::new(ErrorKind::DataConversion, error)
287    }
288}
289
290impl From<base64::DecodeError> for Error {
291    fn from(error: base64::DecodeError) -> Self {
292        Self::new(ErrorKind::DataConversion, error)
293    }
294}
295
296#[cfg(feature = "json")]
297impl From<serde_json::Error> for Error {
298    fn from(error: serde_json::Error) -> Self {
299        Self::new(ErrorKind::DataConversion, error)
300    }
301}
302
303impl From<std::string::FromUtf8Error> for Error {
304    fn from(error: std::string::FromUtf8Error) -> Self {
305        Self::new(ErrorKind::DataConversion, error)
306    }
307}
308
309impl From<std::str::Utf8Error> for Error {
310    fn from(error: std::str::Utf8Error) -> Self {
311        Self::new(ErrorKind::DataConversion, error)
312    }
313}
314
315impl From<url::ParseError> for Error {
316    fn from(error: url::ParseError) -> Self {
317        Self::new(ErrorKind::DataConversion, error)
318    }
319}
320
321impl From<core::convert::Infallible> for Error {
322    fn from(_: core::convert::Infallible) -> Self {
323        panic!("no error")
324    }
325}
326
327impl fmt::Display for Error {
328    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
329        match &self.context {
330            Repr::Simple(kind) => fmt::Display::fmt(&kind, f),
331            Repr::SimpleMessage(_, message) => f.write_str(message),
332            Repr::Custom(Custom { error, .. }) => fmt::Display::fmt(&error, f),
333            Repr::CustomMessage(_, message) => f.write_str(message),
334        }
335    }
336}
337
338/// An extension to the [`Result`] type that easy allows creating [`Error`] values from existing errors.
339///
340/// This trait cannot be implemented on custom types and is meant for usage with `Result`.
341pub trait ResultExt<T>: private::Sealed {
342    /// Creates a new error with the specified [`ErrorKind`].
343    fn with_kind(self, kind: ErrorKind) -> Result<T>
344    where
345        Self: Sized;
346
347    /// Creates a new error with the specified [`ErrorKind`] and an additional message.
348    fn with_context<C>(self, kind: ErrorKind, message: C) -> Result<T>
349    where
350        Self: Sized,
351        C: Into<Cow<'static, str>>;
352
353    /// Creates a new error with the specified [`ErrorKind`] and a function that returns an additional message.
354    fn with_context_fn<F, C>(self, kind: ErrorKind, f: F) -> Result<T>
355    where
356        Self: Sized,
357        F: FnOnce() -> C,
358        C: Into<Cow<'static, str>>;
359}
360
361mod private {
362    pub trait Sealed {}
363
364    impl<T, E> Sealed for std::result::Result<T, E> where E: std::error::Error + Send + Sync + 'static {}
365}
366
367impl<T, E> ResultExt<T> for std::result::Result<T, E>
368where
369    E: std::error::Error + Send + Sync + 'static,
370{
371    fn with_kind(self, kind: ErrorKind) -> Result<T>
372    where
373        Self: Sized,
374    {
375        self.map_err(|e| Error::new(kind, e))
376    }
377
378    fn with_context<C>(self, kind: ErrorKind, message: C) -> Result<T>
379    where
380        Self: Sized,
381        C: Into<Cow<'static, str>>,
382    {
383        self.map_err(|e| Error {
384            context: Repr::CustomMessage(
385                Custom {
386                    error: Box::new(e),
387                    kind,
388                },
389                message.into(),
390            ),
391            backtrace: capture_backtrace(),
392        })
393    }
394
395    fn with_context_fn<F, C>(self, kind: ErrorKind, f: F) -> Result<T>
396    where
397        Self: Sized,
398        F: FnOnce() -> C,
399        C: Into<Cow<'static, str>>,
400    {
401        self.with_context(kind, f())
402    }
403}
404
405#[inline(always)]
406fn capture_backtrace() -> Option<Box<Backtrace>> {
407    let backtrace = Backtrace::capture();
408    if backtrace.status() == BacktraceStatus::Captured {
409        Some(Box::new(backtrace))
410    } else {
411        None
412    }
413}
414
415#[derive(Debug)]
416enum Repr {
417    Simple(ErrorKind),
418    SimpleMessage(ErrorKind, Cow<'static, str>),
419    Custom(Custom),
420    CustomMessage(Custom, Cow<'static, str>),
421}
422
423#[derive(Debug)]
424struct Custom {
425    kind: ErrorKind,
426    error: Box<dyn std::error::Error + Send + Sync>,
427}
428
429#[cfg(test)]
430mod tests {
431    use super::*;
432    use std::io;
433
434    #[allow(
435        dead_code,
436        unconditional_recursion,
437        clippy::extra_unused_type_parameters
438    )]
439    fn ensure_send<T: Send>() {
440        ensure_send::<Error>();
441    }
442
443    #[derive(thiserror::Error, Debug)]
444    enum IntermediateError {
445        #[error("second error")]
446        Io(#[from] std::io::Error),
447    }
448
449    fn create_error() -> Error {
450        // Create a nested std::io::Error
451        let inner = io::Error::new(io::ErrorKind::BrokenPipe, "third error");
452        let inner: IntermediateError = inner.into();
453        let inner = io::Error::new(io::ErrorKind::ConnectionAborted, inner);
454
455        // Wrap that io::Error in this crate's Error type
456        Error::new(ErrorKind::Io, inner)
457    }
458
459    #[test]
460    fn errors_display_properly() {
461        let error = create_error();
462
463        // Generate the display and error chain
464        let mut error: &dyn std::error::Error = &error;
465        let display = format!("{error}");
466        let mut errors = vec![];
467        while let Some(cause) = error.source() {
468            errors.push(format!("{cause}"));
469            error = cause;
470        }
471
472        assert_eq!(display, "second error");
473        assert_eq!(errors.join(","), "second error,third error");
474
475        let inner = io::Error::new(io::ErrorKind::BrokenPipe, "third error");
476        let error: Result<()> = std::result::Result::<(), std::io::Error>::Err(inner)
477            .with_context(ErrorKind::Io, "oh no broken pipe!");
478        assert_eq!(format!("{}", error.unwrap_err()), "oh no broken pipe!");
479    }
480
481    #[test]
482    fn downcasting_works() {
483        let error = &create_error() as &dyn std::error::Error;
484        assert!(error.is::<Error>());
485        let downcasted = error
486            .source()
487            .unwrap()
488            .downcast_ref::<std::io::Error>()
489            .unwrap();
490        assert_eq!(format!("{downcasted}"), "second error");
491    }
492
493    #[test]
494    fn turn_into_inner_error() {
495        let error = create_error();
496        let inner = error.into_inner().unwrap();
497        let inner = inner.downcast_ref::<std::io::Error>().unwrap();
498        assert_eq!(format!("{inner}"), "second error");
499
500        let error = create_error();
501        let inner = error.get_ref().unwrap();
502        let inner = inner.downcast_ref::<std::io::Error>().unwrap();
503        assert_eq!(format!("{inner}"), "second error");
504
505        let mut error = create_error();
506        let inner = error.get_mut().unwrap();
507        let inner = inner.downcast_ref::<std::io::Error>().unwrap();
508        assert_eq!(format!("{inner}"), "second error");
509    }
510
511    #[test]
512    fn set_result_kind() {
513        let result = std::result::Result::<(), _>::Err(create_error());
514        let result = result.with_kind(ErrorKind::Io);
515        assert_eq!(&ErrorKind::Io, result.unwrap_err().kind());
516    }
517
518    #[test]
519    fn backtrace_captured_when_enabled() {
520        let error = Error::new(ErrorKind::Other, "test error");
521        if std::env::var("RUST_BACKTRACE").is_ok() {
522            assert!(error.backtrace.is_some());
523        } else {
524            assert!(error.backtrace.is_none());
525        }
526    }
527}