rquickjs_core/
result.rs

1use std::{
2    error::Error as StdError,
3    ffi::{CString, FromBytesWithNulError, NulError},
4    fmt::{self, Display, Formatter, Result as FmtResult},
5    io::Error as IoError,
6    panic,
7    panic::UnwindSafe,
8    str::{FromStr, Utf8Error},
9    string::FromUtf8Error,
10};
11
12#[cfg(feature = "futures")]
13use crate::context::AsyncContext;
14use crate::value::array_buffer::AsSliceError;
15use crate::{
16    atom::PredefinedAtom, qjs, runtime::UserDataError, value::exception::ERROR_FORMAT_STR, Context,
17    Ctx, Exception, Object, StdResult, StdString, Type, Value,
18};
19
20/// Result type used throughout the library.
21pub type Result<T> = StdResult<T, Error>;
22
23/// Result type containing an the JavaScript exception if there was one.
24pub type CaughtResult<'js, T> = StdResult<T, CaughtError<'js>>;
25
26#[derive(Debug)]
27pub enum BorrowError {
28    /// The object was not writable
29    NotWritable,
30    /// The object was already borrowed in a way that prevents borrowing again.
31    AlreadyBorrowed,
32    /// The object could only be used once and was used already.
33    AlreadyUsed,
34}
35
36impl fmt::Display for BorrowError {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match *self {
39            BorrowError::NotWritable => write!(f, "tried to borrow a value which is not writable"),
40            BorrowError::AlreadyBorrowed => {
41                write!(f, "can't borrow a value as it is already borrowed")
42            }
43            BorrowError::AlreadyUsed => {
44                write!(
45                    f,
46                    "tried to use a value, which can only be used once, again."
47                )
48            }
49        }
50    }
51}
52
53impl std::error::Error for BorrowError {}
54
55/// Error type of the library.
56#[derive(Debug)]
57#[non_exhaustive]
58pub enum Error {
59    /// Could not allocate memory
60    /// This is generally only triggered when out of memory.
61    Allocation,
62    /// A module defined two exported values with the same name.
63    DuplicateExports,
64    /// Tried to export a entry which was not previously declared.
65    InvalidExport,
66    /// Found a string with a internal null byte while converting
67    /// to C string.
68    InvalidString(NulError),
69    /// Found a string with a internal null byte while converting
70    /// to C string.
71    InvalidCStr(FromBytesWithNulError),
72    /// String from rquickjs was not UTF-8
73    Utf8(Utf8Error),
74    /// An io error
75    Io(IoError),
76    /// An error happened while trying to borrow a Rust class object.
77    ClassBorrow(BorrowError),
78    /// An error happened while trying to borrow a Rust function.
79    FunctionBorrow(BorrowError),
80    /// An exception raised by QuickJS itself.
81    /// The actual JavaScript value can be retrieved by calling [`Ctx::catch`].
82    ///
83    /// When returned from a callback the JavaScript will continue to unwind with the current
84    /// error.
85    Exception,
86    /// Error converting from JavaScript to a Rust type.
87    FromJs {
88        from: &'static str,
89        to: &'static str,
90        message: Option<StdString>,
91    },
92    /// Error converting to JavaScript from a Rust type.
93    IntoJs {
94        from: &'static str,
95        to: &'static str,
96        message: Option<StdString>,
97    },
98    /// Error matching of function arguments
99    MissingArgs {
100        expected: usize,
101        given: usize,
102    },
103    TooManyArgs {
104        expected: usize,
105        given: usize,
106    },
107    #[cfg(feature = "loader")]
108    /// Error when resolving js module
109    Resolving {
110        base: StdString,
111        name: StdString,
112        message: Option<StdString>,
113    },
114    #[cfg(feature = "loader")]
115    /// Error when loading js module
116    Loading {
117        name: StdString,
118        message: Option<StdString>,
119    },
120    AsSlice(AsSliceError),
121    /// Error when restoring a Persistent in a runtime other than the original runtime.
122    UnrelatedRuntime,
123    /// An error returned by a blocked on promise if block on the promise would result in a dead
124    /// lock.
125    WouldBlock,
126    /// An error related to userdata
127    UserData(UserDataError<()>),
128    /// An error from QuickJS from which the specifics are unknown.
129    /// Should eventually be removed as development progresses.
130    Unknown,
131}
132
133impl Error {
134    #[cfg(feature = "loader")]
135    /// Create resolving error
136    pub fn new_resolving<B, N>(base: B, name: N) -> Self
137    where
138        StdString: From<B> + From<N>,
139    {
140        Error::Resolving {
141            base: base.into(),
142            name: name.into(),
143            message: None,
144        }
145    }
146
147    #[cfg(feature = "loader")]
148    /// Create resolving error with message
149    pub fn new_resolving_message<B, N, M>(base: B, name: N, msg: M) -> Self
150    where
151        StdString: From<B> + From<N> + From<M>,
152    {
153        Error::Resolving {
154            base: base.into(),
155            name: name.into(),
156            message: Some(msg.into()),
157        }
158    }
159
160    #[cfg(feature = "loader")]
161    /// Returns whether the error is a resolving error
162    pub fn is_resolving(&self) -> bool {
163        matches!(self, Error::Resolving { .. })
164    }
165
166    #[cfg(feature = "loader")]
167    /// Create loading error
168    pub fn new_loading<N>(name: N) -> Self
169    where
170        StdString: From<N>,
171    {
172        Error::Loading {
173            name: name.into(),
174            message: None,
175        }
176    }
177
178    #[cfg(feature = "loader")]
179    /// Create loading error
180    pub fn new_loading_message<N, M>(name: N, msg: M) -> Self
181    where
182        StdString: From<N> + From<M>,
183    {
184        Error::Loading {
185            name: name.into(),
186            message: Some(msg.into()),
187        }
188    }
189
190    #[cfg(feature = "loader")]
191    /// Returns whether the error is a loading error
192    pub fn is_loading(&self) -> bool {
193        matches!(self, Error::Loading { .. })
194    }
195
196    /// Returns whether the error is a QuickJS generated exception.
197    pub fn is_exception(&self) -> bool {
198        matches!(self, Error::Exception)
199    }
200
201    /// Create from JS conversion error
202    pub fn new_from_js(from: &'static str, to: &'static str) -> Self {
203        Error::FromJs {
204            from,
205            to,
206            message: None,
207        }
208    }
209
210    /// Create from JS conversion error with message
211    pub fn new_from_js_message<M>(from: &'static str, to: &'static str, msg: M) -> Self
212    where
213        StdString: From<M>,
214    {
215        Error::FromJs {
216            from,
217            to,
218            message: Some(msg.into()),
219        }
220    }
221
222    /// Create into JS conversion error
223    pub fn new_into_js(from: &'static str, to: &'static str) -> Self {
224        Error::IntoJs {
225            from,
226            to,
227            message: None,
228        }
229    }
230
231    /// Create into JS conversion error with message
232    pub fn new_into_js_message<M>(from: &'static str, to: &'static str, msg: M) -> Self
233    where
234        StdString: From<M>,
235    {
236        Error::IntoJs {
237            from,
238            to,
239            message: Some(msg.into()),
240        }
241    }
242
243    /// Returns whether the error is a from JS conversion error
244    pub fn is_from_js(&self) -> bool {
245        matches!(self, Self::FromJs { .. })
246    }
247
248    /// Returns whether the error is a from JS to JS type conversion error
249    pub fn is_from_js_to_js(&self) -> bool {
250        matches!(self, Self::FromJs { to, .. } if Type::from_str(to).is_ok())
251    }
252
253    /// Returns whether the error is an into JS conversion error
254    pub fn is_into_js(&self) -> bool {
255        matches!(self, Self::IntoJs { .. })
256    }
257
258    /// Return whether the error is an function args mismatch error
259    pub fn is_num_args(&self) -> bool {
260        matches!(self, Self::TooManyArgs { .. } | Self::MissingArgs { .. })
261    }
262
263    /// Optimized conversion to [`CString`]
264    pub(crate) fn to_cstring(&self) -> CString {
265        // stringify error with NUL at end
266        let mut message = format!("{self}\0").into_bytes();
267
268        message.pop(); // pop last NUL because CString add this later
269
270        // TODO: Replace by `CString::from_vec_with_nul_unchecked` later when it will be stabilized
271        unsafe { CString::from_vec_unchecked(message) }
272    }
273
274    /// Throw an exception
275    pub(crate) fn throw(&self, ctx: &Ctx) -> qjs::JSValue {
276        use Error::*;
277        match self {
278            Exception => qjs::JS_EXCEPTION,
279            Allocation => unsafe { qjs::JS_ThrowOutOfMemory(ctx.as_ptr()) },
280            InvalidString(_)
281            | Utf8(_)
282            | FromJs { .. }
283            | IntoJs { .. }
284            | TooManyArgs { .. }
285            | MissingArgs { .. } => {
286                let message = self.to_cstring();
287                unsafe {
288                    qjs::JS_ThrowTypeError(
289                        ctx.as_ptr(),
290                        ERROR_FORMAT_STR.as_ptr(),
291                        message.as_ptr(),
292                    )
293                }
294            }
295            AsSlice(_) => {
296                let message = self.to_cstring();
297                unsafe {
298                    qjs::JS_ThrowReferenceError(
299                        ctx.as_ptr(),
300                        ERROR_FORMAT_STR.as_ptr(),
301                        message.as_ptr(),
302                    )
303                }
304            }
305            #[cfg(feature = "loader")]
306            Resolving { .. } | Loading { .. } => {
307                let message = self.to_cstring();
308                unsafe {
309                    qjs::JS_ThrowReferenceError(
310                        ctx.as_ptr(),
311                        ERROR_FORMAT_STR.as_ptr(),
312                        message.as_ptr(),
313                    )
314                }
315            }
316            Unknown => {
317                let message = self.to_cstring();
318                unsafe {
319                    qjs::JS_ThrowInternalError(
320                        ctx.as_ptr(),
321                        ERROR_FORMAT_STR.as_ptr(),
322                        message.as_ptr(),
323                    )
324                }
325            }
326            error => {
327                unsafe {
328                    let value = qjs::JS_NewError(ctx.as_ptr());
329                    if qjs::JS_VALUE_GET_NORM_TAG(value) == qjs::JS_TAG_EXCEPTION {
330                        //allocation error happened, can't raise error properly. just immediately
331                        //return
332                        return value;
333                    }
334                    let obj = Object::from_js_value(ctx.clone(), value);
335                    match obj.set(PredefinedAtom::Message, error.to_string()) {
336                        Ok(_) => {}
337                        Err(Error::Exception) => return qjs::JS_EXCEPTION,
338                        Err(e) => {
339                            panic!("generated error while throwing error: {}", e);
340                        }
341                    }
342                    let js_val = (obj).into_js_value();
343                    qjs::JS_Throw(ctx.as_ptr(), js_val)
344                }
345            }
346        }
347    }
348}
349
350impl StdError for Error {}
351
352impl Display for Error {
353    fn fmt(&self, f: &mut Formatter) -> FmtResult {
354        match self {
355            Error::Allocation => "Allocation failed while creating object".fmt(f)?,
356            Error::DuplicateExports => {
357                "Tried to export two values with the same name from one module".fmt(f)?
358            }
359            Error::InvalidExport => {
360                "Tried to export a value which was not previously declared".fmt(f)?
361            }
362            Error::InvalidString(error) => {
363                "String contained internal null bytes: ".fmt(f)?;
364                error.fmt(f)?;
365            }
366            Error::InvalidCStr(error) => {
367                "CStr didn't end in a null byte: ".fmt(f)?;
368                error.fmt(f)?;
369            }
370            Error::Utf8(error) => {
371                "Conversion from string failed: ".fmt(f)?;
372                error.fmt(f)?;
373            }
374            Error::Unknown => "QuickJS library created a unknown error".fmt(f)?,
375            Error::Exception => "Exception generated by QuickJS".fmt(f)?,
376            Error::FromJs { from, to, message } => {
377                "Error converting from js '".fmt(f)?;
378                from.fmt(f)?;
379                "' into type '".fmt(f)?;
380                to.fmt(f)?;
381                "'".fmt(f)?;
382                if let Some(message) = message {
383                    if !message.is_empty() {
384                        ": ".fmt(f)?;
385                        message.fmt(f)?;
386                    }
387                }
388            }
389            Error::IntoJs { from, to, message } => {
390                "Error converting from '".fmt(f)?;
391                from.fmt(f)?;
392                "' into js '".fmt(f)?;
393                to.fmt(f)?;
394                "'".fmt(f)?;
395                if let Some(message) = message {
396                    if !message.is_empty() {
397                        ": ".fmt(f)?;
398                        message.fmt(f)?;
399                    }
400                }
401            }
402            Error::MissingArgs { expected, given } => {
403                "Error calling function with ".fmt(f)?;
404                given.fmt(f)?;
405                " argument(s) while ".fmt(f)?;
406                expected.fmt(f)?;
407                " where expected".fmt(f)?;
408            }
409            Error::TooManyArgs { expected, given } => {
410                "Error calling function with ".fmt(f)?;
411                given.fmt(f)?;
412                " argument(s), function is exhaustive and cannot be called with more then "
413                    .fmt(f)?;
414                expected.fmt(f)?;
415                " arguments".fmt(f)?;
416            }
417            #[cfg(feature = "loader")]
418            Error::Resolving {
419                base,
420                name,
421                message,
422            } => {
423                "Error resolving module '".fmt(f)?;
424                name.fmt(f)?;
425                "' from '".fmt(f)?;
426                base.fmt(f)?;
427                "'".fmt(f)?;
428                if let Some(message) = message {
429                    if !message.is_empty() {
430                        ": ".fmt(f)?;
431                        message.fmt(f)?;
432                    }
433                }
434            }
435            #[cfg(feature = "loader")]
436            Error::Loading { name, message } => {
437                "Error loading module '".fmt(f)?;
438                name.fmt(f)?;
439                "'".fmt(f)?;
440                if let Some(message) = message {
441                    if !message.is_empty() {
442                        ": ".fmt(f)?;
443                        message.fmt(f)?;
444                    }
445                }
446            }
447            Error::Io(error) => {
448                "IO Error: ".fmt(f)?;
449                error.fmt(f)?;
450            }
451            Error::ClassBorrow(x) => {
452                "Error borrowing class: ".fmt(f)?;
453                x.fmt(f)?;
454            }
455            Error::FunctionBorrow(x) => {
456                "Error borrowing function: ".fmt(f)?;
457                x.fmt(f)?;
458            }
459            Error::WouldBlock => "Error blocking on a promise resulted in a dead lock".fmt(f)?,
460            Error::UserData(x) => x.fmt(f)?,
461            Error::AsSlice(x) => {
462                "Could not convert array buffer to slice: ".fmt(f)?;
463                x.fmt(f)?;
464            }
465            Error::UnrelatedRuntime => "Restoring Persistent in an unrelated runtime".fmt(f)?,
466        }
467        Ok(())
468    }
469}
470
471macro_rules! from_impls {
472    ($($type:ty => $variant:ident,)*) => {
473        $(
474            impl From<$type> for Error {
475                fn from(error: $type) -> Self {
476                    Error::$variant(error)
477                }
478            }
479        )*
480    };
481}
482
483from_impls! {
484    NulError => InvalidString,
485    FromBytesWithNulError => InvalidCStr,
486    Utf8Error => Utf8,
487    IoError => Io,
488}
489
490impl From<FromUtf8Error> for Error {
491    fn from(error: FromUtf8Error) -> Self {
492        Error::Utf8(error.utf8_error())
493    }
494}
495
496impl<T> From<UserDataError<T>> for Error {
497    fn from(_: UserDataError<T>) -> Self {
498        Error::UserData(UserDataError(()))
499    }
500}
501
502impl From<AsSliceError> for Error {
503    fn from(value: AsSliceError) -> Self {
504        Error::AsSlice(value)
505    }
506}
507
508/// An error type containing possible thrown exception values.
509#[derive(Debug)]
510pub enum CaughtError<'js> {
511    /// Error wasn't an exception
512    Error(Error),
513    /// Error was an exception and an instance of Error
514    Exception(Exception<'js>),
515    /// Error was an exception but not an instance of Error.
516    Value(Value<'js>),
517}
518
519impl<'js> Display for CaughtError<'js> {
520    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
521        match *self {
522            CaughtError::Error(ref e) => e.fmt(f),
523            CaughtError::Exception(ref e) => e.fmt(f),
524            CaughtError::Value(ref e) => {
525                writeln!(f, "Exception generated by quickjs: {e:?}")
526            }
527        }
528    }
529}
530
531impl<'js> StdError for CaughtError<'js> {}
532
533impl<'js> CaughtError<'js> {
534    /// Create a `CaughtError` from an [`Error`], retrieving the error value from `Ctx` if there
535    /// was one.
536    pub fn from_error(ctx: &Ctx<'js>, error: Error) -> Self {
537        if let Error::Exception = error {
538            let value = ctx.catch();
539            if let Some(ex) = value
540                .as_object()
541                .and_then(|x| Exception::from_object(x.clone()))
542            {
543                CaughtError::Exception(ex)
544            } else {
545                CaughtError::Value(value)
546            }
547        } else {
548            CaughtError::Error(error)
549        }
550    }
551
552    /// Turn a `Result` with [`Error`] into a result with [`CaughtError`] retrieving the error
553    /// value from the context if there was one.
554    pub fn catch<T>(ctx: &Ctx<'js>, error: Result<T>) -> CaughtResult<'js, T> {
555        error.map_err(|error| Self::from_error(ctx, error))
556    }
557
558    /// Put the possible caught value back as the current error and turn the [`CaughtError`] into [`Error`]
559    pub fn throw(self, ctx: &Ctx<'js>) -> Error {
560        match self {
561            CaughtError::Error(e) => e,
562            CaughtError::Exception(ex) => ctx.throw(ex.into_value()),
563            CaughtError::Value(ex) => ctx.throw(ex),
564        }
565    }
566
567    /// Returns whether self is of variant `CaughtError::Exception`.
568    pub fn is_exception(&self) -> bool {
569        matches!(self, CaughtError::Exception(_))
570    }
571
572    /// Returns whether self is of variant `CaughtError::Exception` or `CaughtError::Value`.
573    pub fn is_js_error(&self) -> bool {
574        matches!(self, CaughtError::Exception(_) | CaughtError::Value(_))
575    }
576}
577
578/// Extension trait to easily turn results with [`Error`] into results with [`CaughtError`]
579/// # Usage
580/// ```
581/// # use rquickjs::{Error, Context, Runtime, CaughtError};
582/// # let rt = Runtime::new().unwrap();
583/// # let ctx = Context::full(&rt).unwrap();
584/// # ctx.with(|ctx|{
585/// use rquickjs::CatchResultExt;
586///
587/// if let Err(CaughtError::Value(err)) = ctx.eval::<(),_>("throw 3").catch(&ctx){
588///     assert_eq!(err.as_int(),Some(3));
589/// # }else{
590/// #    panic!()
591/// }
592/// # });
593/// ```
594pub trait CatchResultExt<'js, T> {
595    fn catch(self, ctx: &Ctx<'js>) -> CaughtResult<'js, T>;
596}
597
598impl<'js, T> CatchResultExt<'js, T> for Result<T> {
599    fn catch(self, ctx: &Ctx<'js>) -> CaughtResult<'js, T> {
600        CaughtError::catch(ctx, self)
601    }
602}
603
604/// Extension trait to easily turn results with [`CaughtError`] into results with [`Error`]
605///
606/// Calling throw on a `CaughtError` will set the current error to the one contained in
607/// `CaughtError` if such a value exists and then turn `CaughtError` into `Error`.
608pub trait ThrowResultExt<'js, T> {
609    fn throw(self, ctx: &Ctx<'js>) -> Result<T>;
610}
611
612impl<'js, T> ThrowResultExt<'js, T> for CaughtResult<'js, T> {
613    fn throw(self, ctx: &Ctx<'js>) -> Result<T> {
614        self.map_err(|e| e.throw(ctx))
615    }
616}
617
618/// A error raised from running a pending job
619/// Contains the context from which the error was raised.
620///
621/// Use `Ctx::catch` to retrieve the error.
622#[derive(Clone)]
623pub struct JobException(pub Context);
624
625impl fmt::Debug for JobException {
626    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
627        f.debug_tuple("JobException")
628            .field(&"TODO: Context")
629            .finish()
630    }
631}
632
633impl Display for JobException {
634    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
635        write!(f, "Job raised an exception")?;
636        // TODO print the error?
637        Ok(())
638    }
639}
640
641/// A error raised from running a pending job
642/// Contains the context from which the error was raised.
643///
644/// Use `Ctx::catch` to retrieve the error.
645#[cfg(feature = "futures")]
646#[derive(Clone)]
647pub struct AsyncJobException(pub AsyncContext);
648
649#[cfg(feature = "futures")]
650impl fmt::Debug for AsyncJobException {
651    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
652        f.debug_tuple("AsyncJobException")
653            .field(&"TODO: Context")
654            .finish()
655    }
656}
657
658#[cfg(feature = "futures")]
659impl Display for AsyncJobException {
660    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
661        write!(f, "Async job raised an exception")?;
662        // TODO print the error?
663        Ok(())
664    }
665}
666
667impl<'js> Ctx<'js> {
668    pub(crate) fn handle_panic<F>(&self, f: F) -> qjs::JSValue
669    where
670        F: FnOnce() -> qjs::JSValue + UnwindSafe,
671    {
672        unsafe {
673            match panic::catch_unwind(f) {
674                Ok(x) => x,
675                Err(e) => {
676                    self.get_opaque().set_panic(e);
677                    qjs::JS_Throw(self.as_ptr(), qjs::JS_MKVAL(qjs::JS_TAG_EXCEPTION, 0))
678                }
679            }
680        }
681    }
682
683    /// Handle possible exceptions in [`JSValue`]'s and turn them into errors
684    /// Will return the [`JSValue`] if it is not an exception
685    ///
686    /// # Safety
687    /// Assumes to have ownership of the [`JSValue`]
688    pub(crate) unsafe fn handle_exception(&self, js_val: qjs::JSValue) -> Result<qjs::JSValue> {
689        if qjs::JS_VALUE_GET_NORM_TAG(js_val) != qjs::JS_TAG_EXCEPTION {
690            Ok(js_val)
691        } else {
692            if let Some(x) = self.get_opaque().take_panic() {
693                panic::resume_unwind(x)
694            }
695            Err(Error::Exception)
696        }
697    }
698
699    /// Returns [`Error::Exception`] if there is no existing panic,
700    /// otherwise continues panicking.
701    pub(crate) fn raise_exception(&self) -> Error {
702        // Safety
703        unsafe {
704            if let Some(x) = self.get_opaque().take_panic() {
705                panic::resume_unwind(x)
706            }
707            Error::Exception
708        }
709    }
710}