rquickjs_core/
result.rs

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