Skip to main content

quickjs_runtime/quickjs_utils/
errors.rs

1//! utils for getting and reporting exceptions
2
3use crate::jsutils::JsError;
4use crate::quickjs_utils::{objects, primitives};
5use crate::quickjsrealmadapter::QuickJsRealmAdapter;
6use crate::quickjsvalueadapter::{QuickJsValueAdapter, TAG_EXCEPTION};
7use libquickjs_sys as q;
8
9/// Get the last exception from the runtime, and if present, convert it to an JsError.
10/// # Safety
11/// When passing a context pointer please make sure the corresponding QuickJsContext is still valid
12pub unsafe fn get_exception(context: *mut q::JSContext) -> Option<JsError> {
13    log::trace!("get_exception");
14    let exception_val = q::JS_GetException(context);
15    log::trace!("get_exception / 2");
16    let exception_ref =
17        QuickJsValueAdapter::new(context, exception_val, false, true, "errors::get_exception");
18
19    if exception_ref.is_null() {
20        None
21    } else {
22        let err = if exception_ref.is_exception() {
23            JsError::new_str("Could not get exception from runtime")
24        } else if exception_ref.is_object() {
25            error_to_js_error(context, &exception_ref)
26        } else {
27            match exception_ref.to_string() {
28                Ok(s) => JsError::new_string(s),
29                Err(ex) => {
30                    JsError::new_string(format!("Could not determine error due to error: {ex:?}"))
31                }
32            }
33        };
34        Some(err)
35    }
36}
37
38/// convert an instance of Error to JsError
39/// # Safety
40/// When passing a context pointer please make sure the corresponding QuickJsContext is still valid
41pub unsafe fn error_to_js_error(
42    context: *mut q::JSContext,
43    exception_ref: &QuickJsValueAdapter,
44) -> JsError {
45    log::trace!("error_to_js_error");
46    let name_ref = objects::get_property(context, exception_ref, "name")
47        .ok()
48        .unwrap();
49    let name_string = primitives::to_string(context, &name_ref).ok().unwrap();
50    let message_ref = objects::get_property(context, exception_ref, "message")
51        .ok()
52        .unwrap();
53    let message_string = primitives::to_string(context, &message_ref).ok().unwrap();
54    let stack_ref = objects::get_property(context, exception_ref, "stack")
55        .ok()
56        .unwrap();
57    let mut stack_string = "".to_string();
58
59    let stack2_ref = objects::get_property(context, exception_ref, "stack2")
60        .ok()
61        .unwrap();
62    if stack2_ref.is_string() {
63        stack_string.push_str(
64            primitives::to_string(context, &stack2_ref)
65                .ok()
66                .unwrap()
67                .as_str(),
68        );
69    }
70
71    if stack_ref.is_string() {
72        let stack_str = primitives::to_string(context, &stack_ref).ok().unwrap();
73        #[cfg(feature = "typescript")]
74        let stack_str = crate::typescript::unmap_stack_trace(stack_str.as_str());
75
76        stack_string.push_str(stack_str.as_str());
77    }
78
79    let cause_ref = objects::get_property(context, exception_ref, "cause")
80        .ok()
81        .unwrap();
82    // add cause as cause but also extend the stack trace with the cause stack trace
83
84    if cause_ref.is_null_or_undefined() {
85        JsError::new(name_string, message_string, stack_string)
86    } else {
87        QuickJsRealmAdapter::with_context(context, |realm| {
88            let cause_str = cause_ref.to_string().ok().unwrap();
89            let cause_jsvf = realm.to_js_value_facade(&cause_ref).ok().unwrap();
90
91            stack_string.push_str("Caused by: ");
92            stack_string.push_str(cause_str.as_str());
93
94            JsError::new2(name_string, message_string, stack_string, cause_jsvf)
95        })
96    }
97}
98
99/// Create a new Error object
100/// # Safety
101/// When passing a context pointer please make sure the corresponding QuickJsContext is still valid
102pub unsafe fn new_error(
103    context: *mut q::JSContext,
104    name: &str,
105    message: &str,
106    stack: &str,
107) -> Result<QuickJsValueAdapter, JsError> {
108    let obj = q::JS_NewError(context);
109    let obj_ref = QuickJsValueAdapter::new(
110        context,
111        obj,
112        false,
113        true,
114        format!("new_error {name}").as_str(),
115    );
116    objects::set_property(
117        context,
118        &obj_ref,
119        "message",
120        &primitives::from_string(context, message)?,
121    )?;
122    objects::set_property(
123        context,
124        &obj_ref,
125        "name",
126        &primitives::from_string(context, name)?,
127    )?;
128    objects::set_property(
129        context,
130        &obj_ref,
131        "stack2",
132        &primitives::from_string(context, stack)?,
133    )?;
134    Ok(obj_ref)
135}
136
137/// See if a JSValueRef is an Error object
138pub fn is_error_q(q_ctx: &QuickJsRealmAdapter, obj_ref: &QuickJsValueAdapter) -> bool {
139    unsafe { is_error(q_ctx.context, obj_ref) }
140}
141
142/// See if a JSValueRef is an Error object
143/// # Safety
144/// When passing a context pointer please make sure the corresponding QuickJsContext is still valid
145#[allow(unused_variables)]
146pub unsafe fn is_error(context: *mut q::JSContext, obj_ref: &QuickJsValueAdapter) -> bool {
147    if obj_ref.is_object() {
148        #[cfg(feature = "bellard")]
149        {
150            let res = q::JS_IsError(context, *obj_ref.borrow_value());
151            res != 0
152        }
153        #[cfg(feature = "quickjs-ng")]
154        {
155            q::JS_IsError(*obj_ref.borrow_value())
156        }
157    } else {
158        false
159    }
160}
161
162pub fn get_stack(realm: &QuickJsRealmAdapter) -> Result<QuickJsValueAdapter, JsError> {
163    let e = realm.invoke_function_by_name(&[], "Error", &[])?;
164    realm.get_object_property(&e, "stack")
165}
166
167/// Throw an error and get an Exception JSValue to return from native methods
168/// # Safety
169/// When passing a context pointer please make sure the corresponding QuickJsContext is still valid
170pub unsafe fn throw(context: *mut q::JSContext, error: QuickJsValueAdapter) -> q::JSValue {
171    assert!(is_error(context, &error));
172    q::JS_Throw(context, error.clone_value_incr_rc());
173    q::JSValue {
174        #[cfg(feature = "bellard")]
175        u: q::JSValueUnion { uint64: 0 },
176        #[cfg(feature = "quickjs-ng")]
177        u: q::JSValueUnion { int32: 0 },
178        tag: TAG_EXCEPTION,
179    }
180}
181
182#[cfg(test)]
183pub mod tests {
184    use crate::facades::tests::init_test_rt;
185    use crate::jsutils::{JsError, Script};
186    use crate::quickjs_utils::functions;
187    use crate::values::{JsValueConvertable, JsValueFacade};
188    use std::thread;
189    use std::time::Duration;
190
191    #[test]
192    fn test_ex_nat() {
193        // check if stacktrace is preserved when invoking native methods
194
195        let rt = init_test_rt();
196        let res = rt.eval_sync(
197            None,
198            Script::new(
199                "ex.js",
200                "console.log('foo');\nconsole.log('bar');let a = __c_v__ * 7;",
201            ),
202        );
203        let ex = res.expect_err("script should have failed;");
204
205        #[cfg(feature = "bellard")]
206        assert_eq!(ex.get_message(), "'__c_v__' is not defined");
207        #[cfg(feature = "quickjs-ng")]
208        assert_eq!(ex.get_message(), "__c_v__ is not defined");
209    }
210
211    #[test]
212    fn test_ex_cause() {
213        // check if stacktrace is preserved when invoking native methods
214
215        let rt = init_test_rt();
216        let res = rt.eval_sync(
217            None,
218            Script::new(
219                "ex.ts",
220                r#"
221                let a = 2;
222                let b = 3;
223
224                function f1(a, b) {
225                    throw new Error('Could not f1', { cause: 'Sabotage here' });
226                }
227                function f2() {
228                    try {
229                        let r = f1(a, b);
230                    } catch(ex) {
231                        throw new Error('could not f2', { cause: ex});
232                    }
233                }
234                f2()
235                "#,
236            ),
237        );
238        let ex = res.expect_err("script should have failed;");
239
240        assert_eq!(ex.get_message(), "could not f2");
241
242        let complete_err = format!("{ex}");
243        assert!(complete_err.contains("Caused by: Error: Could not f1"));
244        assert!(complete_err.contains("Caused by: Sabotage here"));
245    }
246
247    #[test]
248    fn test_ex0() {
249        // check if stacktrace is preserved when invoking native methods
250
251        let rt = init_test_rt();
252        let res = rt.eval_sync(
253            None,
254            Script::new(
255                "ex.js",
256                "console.log('foo');\nconsole.log('bar');let a = __c_v__ * 7;",
257            ),
258        );
259        let ex = res.expect_err("script should have failed;");
260
261        #[cfg(feature = "bellard")]
262        assert_eq!(ex.get_message(), "'__c_v__' is not defined");
263        #[cfg(feature = "quickjs-ng")]
264        assert_eq!(ex.get_message(), "__c_v__ is not defined");
265    }
266
267    #[test]
268    fn test_ex1() {
269        // check if stacktrace is preserved when invoking native methods
270
271        let rt = init_test_rt();
272        rt.set_function(&[], "test_consume", move |_realm, args| {
273            // args[0] is a function i'll want to call
274            let func_jsvf = &args[0];
275            match func_jsvf {
276                JsValueFacade::JsFunction { cached_function } => {
277                    let _ = cached_function.invoke_function_sync(vec![12.to_js_value_facade()])?;
278                    Ok(0.to_js_value_facade())
279                }
280                _ => Err(JsError::new_str("poof")),
281            }
282        })
283        .expect("could not set function");
284        let s_res = rt.eval_sync(
285            None,
286            Script::new(
287                "test_ex34245.js",
288                "let consumer = function() {
289        console.log('consuming');
290        throw new Error('oh dear stuff failed at line 3 in consumer');
291        };
292        console.log('calling consume from line 6');
293        let a = test_consume(consumer);
294        console.log('should never reach line 7 %s', a)",
295            ),
296        );
297        match s_res {
298            Ok(o) => {
299                log::info!("o = {}", o.stringify());
300            }
301            Err(e) => {
302                log::error!("script failed: {}", e);
303                log::error!("{}", e);
304            }
305        }
306
307        std::thread::sleep(Duration::from_secs(1));
308    }
309
310    #[test]
311    fn test_ex3() {
312        let rt = init_test_rt();
313        rt.eval_sync(
314            None,
315            Script::new(
316                "test_ex3.js",
317                r#"
318
319async function sleep(ms) {
320    return await new Promise((res) => {
321        //setTimeout(res, ms);
322        res();
323    });
324}
325
326async function a() {
327    await b();
328}
329
330async function b() {
331    await sleep(50);
332    return new Promise((res) => {
333        res(c());
334    });
335}
336
337async function c() {
338    await sleep(10);
339    await sleep(10);
340    await sleep(10);
341    await sleep(10);
342    await d();
343}
344
345async function d() {
346    throw Error("poof");
347}
348
349const ap = a();
350ap.catch((ex) => {
351    console.error("The error = %s, stack:%s", ex.message, ex.stack);
352});
353        "#,
354            ),
355        )
356        .expect("script failed");
357        thread::sleep(Duration::from_secs(1));
358    }
359
360    #[test]
361    fn test_ex_stack() {
362        let rt = init_test_rt();
363        rt.exe_rt_task_in_event_loop(|rt| {
364            let realm = rt.get_main_realm();
365            realm
366                .install_closure(
367                    &[],
368                    "myFunc",
369                    |_rt, realm, _this, _args| crate::quickjs_utils::errors::get_stack(realm),
370                    0,
371                )
372                .expect("could not install func");
373
374            let res = realm
375                .eval(Script::new(
376                    "runMyFunc.js",
377                    r#"
378                function a(){
379                    return b();                
380                }
381                function b(){
382                    return myFunc();
383                }
384                a()
385            "#,
386                ))
387                .expect("script failed");
388
389            log::info!("test_ex_stack res = {}", res.to_string().unwrap());
390        });
391    }
392
393    #[test]
394    fn test_ex2() {
395        // check if stacktrace is preserved when invoking native methods
396
397        //simple_logging::log_to_stderr(LevelFilter::Info);
398
399        let rt = init_test_rt();
400        rt.exe_rt_task_in_event_loop(|q_js_rt| {
401            let q_ctx = q_js_rt.get_main_realm();
402
403            q_ctx
404                .eval(Script::new(
405                    "test_ex2_pre.es",
406                    "console.log('before ex test');",
407                ))
408                .expect("test_ex2_pre failed");
409            {
410                let func_ref1 = q_ctx
411                    .eval(Script::new(
412                        "test_ex2f1.es",
413                        "(function(){\nconsole.log('running f1');});",
414                    ))
415                    .expect("script failed");
416                assert!(functions::is_function_q(q_ctx, &func_ref1));
417                let res = functions::call_function_q(q_ctx, &func_ref1, &[], None);
418                match res {
419                    Ok(_) => {}
420                    Err(e) => {
421                        log::error!("func1 failed: {}", e);
422                    }
423                }
424            }
425            // why the f does this fail with a stack overflow if i remove the block above?
426            let func_ref2 = q_ctx
427                .eval(Script::new(
428                    "test_ex2.es",
429                    r#"
430                    const f = function(){
431                        throw Error('poof');
432                    };
433                    f
434                    "#,
435                ))
436                .expect("script failed");
437
438            assert!(functions::is_function_q(q_ctx, &func_ref2));
439            let res = functions::call_function_q(q_ctx, &func_ref2, &[], None);
440            match res {
441                Ok(_) => {}
442                Err(e) => {
443                    log::error!("func2 failed: {}", e);
444                }
445            }
446        });
447
448        #[cfg(feature = "bellard")]
449        {
450            let mjsvf = rt
451                .eval_module_sync(
452                    None,
453                    Script::new(
454                        "test_ex2.es",
455                        r#"
456                                throw Error('poof');
457                                "#,
458                    ),
459                )
460                .map_err(|e| {
461                    log::error!("script compilation failed: {e}");
462                    e
463                })
464                .expect("script compilation failed");
465            match mjsvf {
466                JsValueFacade::JsPromise { cached_promise } => {
467                    let pres = cached_promise
468                        .get_promise_result_sync()
469                        .expect("promise timed out");
470                    match pres {
471                        Ok(m) => {
472                            log::info!("prom resolved to {}", m.stringify())
473                        }
474                        Err(e) => {
475                            log::info!("prom rejected to {}", e.stringify())
476                        }
477                    }
478                }
479                _ => {
480                    panic!("not a prom")
481                }
482            }
483        }
484
485        std::thread::sleep(Duration::from_secs(1));
486    }
487}