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