Error Handling in Elixir
Mix.install([
{:kino, "~> 0.14.0"}
])
With Pattern matching
Its common to use tuples for represent results in Elixir.
You have already seen how it works with File
module
file_path =
Kino.FS.file_path("random_names.csv")
Lets read the file with File.read/1
file_path |> File.read()
We are getting a tuple with
{:ok, content}
Now lets try to open a non existing file
File.read("hello.404")
Now we get another tuple {:error, reason}
Conventions
In elixir its common to represent results like
{:ok, value}
{:error, reason}
you are not limited to number of elements in tuple
the first thing to notice is first element is either :ok
or :error
Now we can do pattern matching aginst this to determine the flow
defmodule UserService do
# Simulating a simple in-memory "database" of users
@users %{
1 => %{id: 1, name: "Alice"},
2 => %{id: 2, name: "Bob"}
}
# Function to fetch a user by ID with tuple-based error handling
def get_user(id) do
case Map.fetch(@users, id) do
{:ok, user} -> {:ok, user}
:error -> {:error, "User not found"}
end
end
# A function that uses `get_user` and handles its response
def print_user_name(id) do
case get_user(id) do
{:ok, user} -> IO.puts("User found: #{user.name}")
{:error, reason} -> IO.puts("Error: #{reason}")
end
end
end
UserService.print_user_name(1)
UserService.print_user_name(4)
Try/Catch/Rescue
This is really uncommon in elixir world, but we can use exceptions
> This is similar to try/catch/finally
block in JS
:foo + 1
Above code throws an error(exception) since we cant add an atom to a number
We can raise error with raise
keyword too
raise "oops"
We can create custom exceptions too
defmodule MyApp.CustomError do
# Defines a custom exception with a message and additional data
defexception [:message, :details]
# Sets a default message if none is provided
def exception(opts) do
message = opts[:message] || "An error occurred in MyApp"
details = opts[:details] || %{}
%MyApp.CustomError{message: message, details: details}
end
end
try/rescue
rescue
can rescue an error when raised
> We can raise exceptions for errors happens during program execution, like invalid inputs etc
result = try do
# Simulating an error
raise "Something went wrong!"
rescue
e in RuntimeError ->
IO.puts("Caught a RuntimeError: #{e.message}")
{:error, :runtime_error}
_ ->
IO.puts("Caught an unknown error")
{:error, :unknown}
end
try/catch
throw
is a way to escape from the control flow, similar to an early exit
> This is also an uncommon pattern, we usually use a porper API to handle the case
result = try do
Enum.each(1..10, fn x ->
if x == 5, do: throw(:found) # Throws a signal when x is 5
end)
:not_found
catch
:throw, :found ->
IO.puts("Value found, stopping early.")
:found
end
Conventions
Most of the time you will use tuples or atoms to represent results instead of using try/catch/rescue
Lets look at an example
File.read/1
returns us a tuple
File.read("bello.txt")
We also have a function called File.read!/1
which raise an exception
File.read!("bello.txt")
So when writing functions its common to use a !
to let caller knows that the function may raise an error