Illegal invocation when calling alert function using js_of_ocaml

I’m using js_of_ocaml (4.0.0) to compile OCaml to JS and using the brr library (0.0.3) to interact with the browser, but I ran into some interesting behavior.

(For reference, here is the original GitHub issue about this.)

Problems with alert

Here is some code for setting up the alert function that is more or less the same as the one from the brr docs:

(* This works! *)
let alert msg =
  let alert = Jv.get Jv.global "alert" in
  ignore @@ Jv.apply alert Jv.[| of_string msg |]

You can use this function with no problems in both dev and release mode.

Switching from get to find breaks things

However, if you switch from get to find (which returns an option rather than raising), you get problems:

(* Works in dev mode, not in release mode *)
let alert msg =
  match Jv.find Jv.global "alert" with
  | None -> prerr_endline "no alert"
  | Some alert -> ignore @@ Jv.apply alert Jv.[| of_string msg |]

When building with dune in dev mode, the above function works fine, however, when building in release mode, you get an Illegal invocation error when trying to use the function.

Strangely, if you put a print call in there, it works fine again, even in release mode.

(* Works in both dev and release mode *)
let alert msg =
  match Jv.find Jv.global "alert" with
  | None -> prerr_endline "no alert"
  | Some alert ->
      (* If you add print here, it works again.... *)
      prerr_endline "yay!";
      ignore @@ Jv.apply alert Jv.[| of_string msg |]

Switching to call from apply works again

In javascript, you can call alert on window in the browser and indeed, this OCaml version works fine in both dev and release modes.

(* Works in both dev and release mode *)
let alert msg =
  match Jv.find Jv.global "window" with
  | None -> prerr_endline "no window"
  | Some window -> ignore @@ Jv.call window "alert" Jv.[| of_string msg |]

Brr function reference

For reference, here is how the functions from the above examples are defined in the brr library. (Taken from here.)

let global = pure_js_expr "globalThis"
external pure_js_expr : string -> 'a = "caml_pure_js_expr"

external get : t -> prop -> t = "caml_js_get"
let find o p = let v = get o p in if is_none v then None else Some v

external call : t -> string -> t array -> 'a = "caml_js_meth_call"
external apply : t -> t array -> 'a = "caml_js_fun_call"

Any thoughts on what may be going on here?

@mooreryan just to make sure people understand things here.

As mentioned in the linked issue alert is not a function but a method of the window object.

So this binding is the only correct one and works perfectly.

To me the other options are just undefined behaviour. So if one is interested in explaining undefined behaviour, speak up !

1 Like

As mentioned in the linked issue alert is not a function but a method of the window object.

I’m curious about any of the methods on the global object (window/global/globalThis) that are also available directly (like alert, print, prompt, etc). Should using Js.get Jv.global "blah" followed by Jv.apply be considered something that will lead to undefined behavior? (Even though in JS I can just write alert("hi") for example in addition to window.alert("hi")?)

In other words, it’s better to use Jv.call on the window object directly rather than relying on something being globally available?

So I dug around more in the javscript generated in dev mode and release mode by js_of_ocaml and figured out what was going on (more or less).

Comparing these two versions of the alert function…

let alert msg =
  let alert = Jv.get Jv.global "alert" in
  ignore @@ Jv.apply alert Jv.[| of_string msg |]

let alert2 msg =
  match Jv.find Jv.global "alert" with
  | None -> ()
  | Some alert -> ignore @@ Jv.apply alert Jv.[| of_string msg |]

In dev mode, both alert and alert2 get translated to JS in such a way that when the actual JS alert function is invoked, it retains its this reference to window (even though they are handled a bit differently to check that alert actually exists).

However, when compiled in release mode, the different handling for the second one causes that call to the JS alert function to lose its original this reference to window. Basically it gets shoved in an array and then called…something like this:

var G = // ... code to get alert function ... 
// ... code to see if alert is defined ...
_ = G_is_undefined ? 0 :  [0, G] 
if (_) _[1]("hello world!") // this will fail with illegal invocation

So basically, when alert is called, this will be the array [0, G] rather than window like it should.

To summarize, if your function depends on this being set to the correct object, you had better not use Jv.apply on it…rather use Jv.call to make sure it is called on the correct object, and thus have the correct this. The fact that both work in dev mode but only one works in release mode seems to be an implementation detail and shouldn’t be relied on.

Of course, @dbuenzli said as much above regarding alert being part of the window object, but at least now I know why the wrong way to do it sometimes works and sometimes doesn’t!

(Didn’t bother to check on the one with the print_endline in there though…)

1 Like

I think the confusion here is that when you write alert() you are not calling a function you are calling the alert() method of the global object (which in the browser happens to be the window object).

1 Like

Yeah that was the confusion. I assumed that since I could call either alert or window.alert in JS I could do the same safely in js_of_ocaml. However, I did not realize the stuff about this before digging into it more, or how sometimes the optimized js_of_ocaml output could sometimes lose the original this context.