Sending (some) functions between OTP versions fails with 'badfun'

29 Apr 2025 11:58 erlang

Did you know that you can send Erlang functions between Erlang nodes? You can. Did you know that the OTP versions need to match?

Start Erlang distribution

We’ll start two Erlang nodes, running different major versions of OTP:

$ source ~/.kerl/erlangs/OTP-26.2.5.3/activate
$ erl -sname otp26

Erlang/OTP 26 [erts-14.2.5.3] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Eshell V14.2.5.3 (press Ctrl+G to abort, type help(). for help)
(otp26@roger-m1)1>
$ source ~/.kerl/erlangs/OTP-27.3.3/activate
$ erl -sname otp27

Erlang/OTP 27 [erts-15.2.6] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]

Eshell V15.2.6 (press Ctrl+G to abort, type help(). for help)
(otp27@roger-m1)1>

Then we’ll connect them into a cluster; run this on the OTP-26 node:

(otp26@roger-m1)1> net_kernel:connect_node('otp27@roger-m1').
true
(otp26@roger-m1)2> nodes().
['otp27@roger-m1']

Register shell processes

To communicate between the two nodes, we’ll need a registered process; we’ll register the shell:

(otp26@roger-m1)3> register(shell, self()).
true
(otp27@roger-m1)1> register(shell, self()).
true

We don’t need to register both, since we’ll only send messages in one direction, but I like the symmetry.

Send a simple message

(otp26@roger-m1)5> {shell, 'otp27@roger-m1'} ! hello.
hello
(otp27@roger-m1)3> flush().
Shell got hello
ok

That works.

Send a simple function

Now to send a function:

(otp26@roger-m1)7> {shell, 'otp27@roger-m1'} ! fun() -> hello end.
#Fun<erl_eval.43.105768164>
(otp27@roger-m1)3> F = receive F -> F end.
#Fun<erl_eval.43.105768164>
(otp27@roger-m1)4> F().
hello

Send a more complex function

That works. What if we send something slightly more complicated?

(otp26@roger-m1)6> {shell, 'otp27@roger-m1'} ! fun(M) -> maps:size(M) end.
#Fun<erl_eval.42.105768164>
(otp27@roger-m1)5> G = receive G -> G end.
#Fun<erl_eval.42.105768164>
(otp27@roger-m1)6> G(#{}).
** exception error: bad function #Fun<shell.5.18354929>

It fails with “bad function” (or badfun), because the serialized function depends on OTP-26, and we’re running OTP-27.