Asynchronous Operations with GenServer

Handling Asynchronous Operations

In the previous lesson, you used GenServer to manage state and saw both call (synchronous) and cast (asynchronous). As a quick reminder: calls wait for a reply; casts do not. Today, you will focus on asynchronous work with a small but practical example — a logger process that accepts fire-and-forget writes and lets you fetch the results later.

Start a Logger Process and Initialize State

defmodule MyLogger do
  use GenServer

  def start_link do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  def init(_) do
    {:ok, []}
  end

Explanation:

  • use GenServer wires up the GenServer behavior.
  • start_link/0 starts the process and registers it under the module name (__MODULE__) for easy access. In this unit, we are returning to using registered names rather than the PID-based approach from Unit 2. Registered names are ideal for "singleton" services (like a logger) where you only need one instance and want to avoid the overhead of passing PIDs around manually.
  • init/1 sets the initial state to an empty list that will store log entries.

Fire-and-Forget Writes With Cast

  def log(message) do
    GenServer.cast(__MODULE__, {:log, message})
  end

  def handle_cast({:log, message}, logs) do
    timestamp = DateTime.utc_now() |> DateTime.to_string()
    new_log = "#{timestamp} - #{message}"
    IO.puts "Logged: #{new_log}"
    {:noreply, [new_log | logs]}
  end

Explanation:

  • log/1 uses GenServer.cast/2, which is asynchronous. The caller does not wait.
  • handle_cast/2 timestamps the message, prints it, and prepends it to the list.
  • We return {:noreply, new_state} because casts do not reply. Prepending is fast; we will reorder when reading.

Read Logs With Call and Run the Flow

  def get_logs do
    GenServer.call(__MODULE__, :get_logs)
  end

  def handle_call(:get_logs, _from, logs) do
    {:reply, Enum.reverse(logs), logs}
  end
end

{:ok, _pid} = MyLogger.start_link()
MyLogger.log("Application started")
MyLogger.log("User logged in")
MyLogger.log("Data processed")
Process.sleep(100) # Ensure logs are processed before fetching
IO.inspect MyLogger.get_logs()

Explanation:

  • get_logs/0 uses GenServer.call/2 to synchronously fetch data.
  • handle_call/3 replies with logs in chronological order by reversing the list.
  • We start the server, send three async logs, briefly sleep to let casts process, then fetch and print the logs.

Summary and Next Steps

You built an asynchronous logger with GenServer:

  • Used cast for fire-and-forget writes.
  • Used call to fetch a consistent, ordered view of the state.
  • Coordinated async work with a small delay to ensure processing completes.
  • On CodeSignal, Elixir/OTP and standard libraries (like DateTime) are already available.

Great work. When you are ready, head to the practice section and apply these ideas to deepen your understanding.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal