Where do I put startup code in Elixir?

By: Derek Kraan / 2019-12-06

Today I want to talk about a pattern that I hear about a lot. It begins with the question “where do I put this code that needs to run when I start up my Elixir application?”

It turns out that we have a lot of options at our disposal. I want to enumerate the popular ones and discuss why you may or may not want to use each particular one.

Application.start/1

The root of every Elixir project is an application file that starts the main supervision tree for the project. If you want some code to run just once when your app starts, you could put your code here. This code will be run in the process that is responsible for starting your application.

defmodule MyApp.Application do

use Application



def start ( _type , _args ) do

MyModule . run_at_start ( )



children = [ ... ]

opts = [ ... ]

Supervisor . start_link ( children , opts )

end

end



GenServer.start_link/3

It’s also possible to put your code in the start_link/3 function of a GenServer module. This code will be run in the supervisor process that is starting your GenServer process!

defmodule MyApp.MyGenServer do

use GenServer



def start_link ( arg ) do

MyModule . run_at_start ( )



GenServer . start_link ( __MODULE__ , arg )

end

end



GenServer.init/1

It’s also possible to put your code in the init/1 callback of a GenServer. This code will be run in the GenServer process.

defmodule MyApp.MyGenServer do

use GenServer



def init ( arg ) do

MyModule . run_at_start ( )



{ :ok , arg }

end

end



GenServer.handle_continue/2

The final option is to run it in a handle_continue/2 callback in a GenServer. This code will also be run in the GenServer process.

defmodule MyApp.MyGenServer do

use GenServer



def init ( arg ) do

{ :ok , arg , { :continue , :oob_init } }

end



def handle_continue ( :oob_init , state ) do

MyModule . run_at_start ( )

{ :noreply , state }

end

end



So which of these options should you use? First you need to ask yourself a couple of questions:

What should happen when MyModule.run_at_start/1 fails? When exactly should MyModule.run_at_start/1 be run?

What will happen when MyModule.run_at_start/1 fails?

This is an important question because each of these four possible solutions will respond in a different way to failure:

Application.start/1 will cause your application to terminate, immediately.

GenServer.start_link/3 will cause its parent supervisor to terminate, immediately.

GenServer.init/1 will cause the GenServer process to terminate, immediately.

GenServer.handle_continue/2 will cause the GenServer process to terminate, immediately.

When exactly should MyModule.run_at_start/1 be run?

Each of these four solutions will run your code at different times while starting the application.

Application.start/1 will run before any other processes in your application have been started.

GenServer.start_link/3 will run immediately before the process itself is started.

GenServer.init/1 will be the first thing inside the process that runs. GenServer.start_link/3 doesn’t return until init/1 has finished, so the supervision tree startup procedure will block here until init/1 returns.

GenServer.handle_continue/2 will be the first thing inside the process that runs after the init/1 callback. Unlike init/1 , GenServer.start_link/3 does not block on handle_continue/2 , so other processes further down in the supervision tree will start while handle_continue/2 is running.

Keeping “supervision” code separate from “application” code

The supervision tree belongs to the error kernel of our application. This is probably part of the reason why the supervisors that we have at our disposal are so simple (and “lacking” in features): less complexity leads to fewer failure modes, and we want our error kernel to be as free of errors as possible. If we follow this logic, then I think we should also conclude that our supervision trees should be designed to be as simple as possible with as few failure modes as possible.

Adding application code in your supervisor (in Application.start/1 or GenServer.start_link/3 ) adds unnecessary complexity that we could put somewhere else (not in the supervision tree itself, but in a supervised process), and should be avoided whenever possible.

So which one should I choose?

For the reasons stated above, I would recommend avoiding putting application code in Application.start/1 and GenServer.start_link/3 and instead aim for GenServer.init/1 or GenServer.handle_continue/2 . Usually we want to avoid crashing the application or supervisor processes. I do, however, know people who put code in Application.start/1 for example. That’s fine if you really want the application to fail if your code fails (for whatever reason).