Skip to main content

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