Elixir tip: :noreply is a killer feature

By: Derek Kraan / 2018-07-19

If you’ve ever looked at the GenServer documentation, you might have noticed something strange.

GenServer's :noreply documentation

At least, this looked very strange to me at first. Why, I wondered, would you ever want to return {:noreply, new_state} in a handle_call/3 callback?

After all, a call to a GenServer that returns {:noreply, new_state} will generate a timeout and crash your program.

It took a while before something clicked and I understood the utility of :noreply for handle_call/3 callbacks. If you read further in the documentation, you’ll see this little nugget:

Returning {:noreply, new_state} does not send a response to the caller and continues the loop with new state new_state. The response must be sent with reply/2.

This means that if you use GenServer.reply(from, reply), you can return {:noreply, new_state} and the caller will still get a response. At first glance this doesn’t seem to be that useful, but let’s explore some of the possibilities.

Doing expensive work “out of band”

It might make sense sometimes to reply early to the caller if there is some expensive work that you want to do that the caller doesn’t actually have to wait for. So you can reply early and do expensive work “out of band”.

Reply early and do expensive work "out of band".

This will allow the caller to receive the reply it’s waiting for early and continue sooner with its work.

Note: since Elixir 1.7 you should use {:continue, term()} instead.

Processing other messages before replying

Sometimes you don’t have the information you need to return at the moment handle_call/3 is called. In that case, you can return {:noreply, new_state} and reply only when you have the correct information.

Send a reply later and process other messages in the meantime.

Now we are starting to see some of the power of :noreply. The caller will wait (until it times out) for the reply it needs, but the GenServer it’s calling into can continue to process messages. This is basically a way to make asynchronous processing look like synchronous processing. We have used this pattern to make calls to an “asynchronous” API look synchronous.

Client sees its request being processed synchronously.

Delegating replying to another process

The coolest part of GenServer.reply/2 is that you don’t have to reply from the process that originally received the call. If you are delegating work to another process, you might as well let that process reply to the call and save yourself a couple of messages.

Responding to a call from another process.

Responding to a call from another process diagram.

This is a great result because it means that we don’t have to burden the GenServer in the middle with the task of responding to the call. This will allow that GenServer to do less work and reduce the likelihood of it becoming a bottleneck.

Making the caller wait

This is the last usage of :noreply that I will share in this blog post. Sometimes it can be useful to make the caller wait until some other process has completed some work, to avoid overloading the system. Then you can delay replying until all the work is done, passing around the from reference as necessary.

Boy yelling

Share this tip with your friends and colleagues!

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

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

Why should every process be supervised?

By: Derek Kraan / 2019-04-01

Implementing Connection Draining in Phoenix

By: Derek Kraan / 2019-01-24

Avoid these OTP Supervision performance pitfalls

By: Derek Kraan / 2019-01-17