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:

  1. What should happen when MyModule.run_at_start/1 fails?
  2. 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).

Of course, code that fails repeatedly in a handle_continue/2 will also bring down your entire application eventually, which is why we actually want to avoid code that might “normally” fail from crashing our processes. “Let it crash” only applies to unexpected errors, after all, which is why for example Ecto doesn’t handle database disconnections by crashing a process.

Drop us a line

Get the ball rolling on your new project, fill out the form below and we'll be in touch quickly.

Recent Posts

Big changes coming in Horde 0.8.0

By: Derek Kraan / 2020-09-03

Highlander, there can be only one

By: Derek Kraan / 2020-04-23

Where do I put startup code in Elixir?

By: Derek Kraan / 2019-12-06

Walkman - isolate your tests from the world

By: Derek Kraan / 2019-07-22

Introducing MerkleMap: improving Horde's performance

By: Derek Kraan / 2019-05-20

What's new in Horde v0.5.0

By: Derek Kraan / 2019-05-06