This:
Notice that when the file does not exist, the version with ! raises an
error. The version without ! is preferred when you want to handle
different outcomes using pattern matching…
will be more clear if you will look at the source code. The ! symbol in a function name is just a syntactic agreement. If you see a function which contains the ! symbol in its name, it means that likely there is a function with the same name, but without ! symbol. Both of these functions will do the same thing, but they will handle errors in a different way.
The function without ! will just return an error to you. You will be need to know a type of error and handle it depends on your type. Look on the broadcast/3 function (variant without !):
def broadcast(server, topic, message) when is_atom(server),
do: call(server, :broadcast, [:none, topic, message])
It just makes call to the given server and will return its result. The broadcast!/3 function will do the same, but: it will call broadcast function without !, will check its result and raise the BroadcastError exception:
def broadcast!(server, topic, message) do
case broadcast(server, topic, message) do
:ok -> :ok
{:error, reason} -> raise BroadcastError, message: reason
end
end