Skip to content

Instantly share code, notes, and snippets.

@clsource
Last active November 10, 2024 00:04
Show Gist options
  • Save clsource/6c31ed20d1bea55e6fce05bc9e1bb7d0 to your computer and use it in GitHub Desktop.
Save clsource/6c31ed20d1bea55e6fce05bc9e1bb7d0 to your computer and use it in GitHub Desktop.
Error Handling Challenge

Bank Account Error Challenge

https://rm4n0s.github.io/posts/3-error-handling-challenge/

The challenge is simple to understand. Print a message to the user based on the path of function calls that produced the error, and not based on the error itself.

print three different messages based on the execution path of the functions and error:

- for f4()->
	f2()->
		f1()->
		ErrBankAccountEmpty
		print "Aand it's gone"
- for f4()->
	f3()->
		f1()->
			ErrInvestmentLost
			print "The money in your account didn't do well"

- for the rest of the cases
	print "This line is for bank members only"

also print any type of stack trace for err

Reference Golang

package main

import (
        "errors"
        "fmt"
        "math/rand/v2"
)

var ErrBankAccountEmpty = errors.New("account-is-empty")
var ErrInvestmentLost = errors.New("investment-lost")

func f1() error {
	n := rand.IntN(9) + 1
	if n%2 == 0 {
		return ErrBankAccountEmpty
	}
	return ErrInvestmentLost
}

func f2() error {
	return f1()
}

func f3() error {
	return f1()
}

func f4() error {
	n := rand.IntN(9) + 1
	if n%2 == 0 {
		return f2()
	}
	return f3()
}

func main() {
	err := f4()
	fmt.Println("Print stack trace", err)
}

Odin Solution

package main

import "core:fmt"
import "core:math/rand"

// my library for type traces
// https://github.com/rm4n0s/trace
import "trace"

F1_Error :: enum {
	None,
	Account_Is_Empty,
	Investment_Lost,
}

F2_Error :: union #shared_nil {
	F1_Error,
}

F3_Error :: union #shared_nil {
	F1_Error,
}


F4_Error :: union #shared_nil {
	F2_Error,
	F3_Error,
}


f1 :: proc() -> F1_Error {
	n := rand.int_max(9) + 1
	if n % 2 == 0 {
		return .Account_Is_Empty
	}
	return .Investment_Lost
}

f2 :: proc() -> F2_Error {
	return f1()
}

f3 :: proc() -> F3_Error {
	return f1()
}


f4 :: proc() -> F4_Error {
	n := rand.int_max(9) + 1
	if n % 2 == 0 {
		return f2()
	}
	return f3()
}


main :: proc() {
	err := f4()

	switch err4 in err {
	case F2_Error:
		switch err2 in err4 {
		case F1_Error:
			#partial switch err2 {
			case .Account_Is_Empty:
				fmt.println("Aand it's gone")
			case:
				fmt.println("This line is for bank members only")
			}
		}

	case F3_Error:
		switch err3 in err4 {
		case F1_Error:
			#partial switch err3 {
			case .Investment_Lost:
				fmt.println("The money in your account didn't do well")
			case:
				fmt.println("This line is for bank members only")
			}

		}
	}

	tr := trace.trace(err)
	fmt.println("Trace:", tr)

	/* Prints randomly:
    Aand it's gone
    Trace: F4_Error -> F2_Error -> F1_Error.Account_Is_Empty

    The money in your account didn't do well
    Trace: F4_Error -> F3_Error -> F1_Error.Investment_Lost

    This line is for bank members only
    Trace: F4_Error -> F3_Error -> F1_Error.Account_Is_Empty
    */
}

ERLANG RESTRICTIONS

As seen in Elixir's Discord: https://discord.com/channels/269508806759809042/269508806759809042/1304465335079931935

Erlang has the following warning box on its documentation page: Developers should rely on stacktrace entries only for debugging purposes. https://www.erlang.org/doc/system/errors.htmlthe-call-stack-back-trace-stacktrace

The VM performs tail call optimization, which does not add new entries to the stacktrace, and also limits stacktraces to a certain depth. Furthermore, compiler options, optimizations, and future changes may add or remove stacktrace entries, causing any code that expects the stacktrace to be in a certain order or contain specific items to fail. The only exception to this rule is the class error with the reason undef which is guaranteed to include the Module, Function and Arity of the attempted function as the first stacktrace entry.

In particular if f1 or any other function is a private function, the erlang compiler may inline the function and thus you will never see a stacktrace entry, because simply the function does not exist

# Error Definitions
defmodule ErrBankAccountEmpty do
defexception message: "Aand it's gone"
end
defmodule ErrInvestmentLost do
defexception message: "The money in your account didn't do well"
end
# Error tuple propagation
defmodule BankAcount do
def f1() do
number = :rand.uniform(9) + 1
case rem(number, 2) do
0 -> {:error, ErrBankAccountEmpty}
_ -> {:error, ErrInvestmentLost}
end
end
def f2() do
f1()
end
def f3() do
f1()
end
def f4() do
number = :rand.uniform(9) + 1
case rem(number, 2) do
0 -> f2()
_ -> f3()
end
end
def main() do
case f4() do
{:error, err} -> raise err
_err -> raise "This line is for bank members only"
end
|> IO.inspect
end
end
BankAcount.main()

Alternative 1

The standard way is using error tuples {:error, message}

** (ErrBankAccountEmpty) Aand it's gone
    challenge.exs:51: BankAcount.main/0
    challenge.exs:58: (file)
** (ErrInvestmentLost) The money in your account didn't do well
    challenge.exs:51: BankAcount.main/0
    challenge.exs:58: (file)
# Error Definitions
defmodule ErrBankAccountEmpty do
defexception message: "Aand it's gone"
end
defmodule ErrInvestmentLost do
defexception message: "The money in your account didn't do well"
end
# The only difference in path f4 -> f2 -> f1 and path f4 -> f3 -> f1
# is both f2 and f3, so we handle the error in those functions
# and raise a new error.
# Finally in main we print the corresponding message
defmodule BankAcount do
def f1() do
number = :rand.uniform(9) + 1
case rem(number, 2) do
0 -> raise ErrBankAccountEmpty
_ -> raise ErrInvestmentLost
end
end
def f2() do
try do
f1()
rescue
_ -> raise ErrBankAccountEmpty
end
end
def f3() do
try do
f1()
rescue
_ -> raise ErrInvestmentLost
end
end
def f4() do
number = :rand.uniform(9) + 1
case rem(number, 2) do
0 -> f2()
_ -> f3()
end
end
def main() do
try do
f4()
rescue
err in [ErrBankAccountEmpty, ErrInvestmentLost] -> raise err
_err -> "This line is for bank members only"
end
|> IO.inspect
end
end
BankAcount.main()

Alternative 2

This one is using try and rescue. Works, but is not the best approach.

https://hexdocs.pm/elixir/try-catch-and-rescue.html

** (ErrBankAccountEmpty) Aand it's gone
    challengev2.exs:38: BankAcount.main/0
    challengev2.exs:45: (file)
** (ErrInvestmentLost) The money in your account didn't do well
    challengev2.exs:38: BankAcount.main/0
    challengev2.exs:45: (file)
# Error Definitions
defmodule ErrBankAccountEmpty do
defexception message: "Aand it's gone"
end
defmodule ErrInvestmentLost do
defexception message: "The money in your account didn't do well"
end
defmodule BankAcount do
def f1(stack) do
number = :rand.uniform(9) + 1
case rem(number, 2) do
0 -> [ErrBankAccountEmpty, [:f1 | stack]]
_ -> [ErrInvestmentLost, [:f1 | stack]]
end
end
def f2(stack) do
f1([:f2 | stack])
end
def f3(stack) do
f1([:f3 | stack])
end
def f4(stack) do
number = :rand.uniform(9) + 1
case rem(number, 2) do
0 -> f2([:f4 | stack])
_ -> f3([:f4 | stack])
end
end
def main() do
case f4([:main]) do
[err, stack] ->
IO.inspect(stack)
raise err
_err -> "This line is for bank members only"
end
|> IO.inspect
end
end
BankAcount.main()

Alternative 3

Since Erlang does not guarantees the callstack, we can store it and can pattern match it later.

[:f1, :f3, :f4, :main]
** (ErrBankAccountEmpty) Aand it's gone
    challengev3.exs:40: BankAcount.main/0
    challengev3.exs:47: (file)
[:f1, :f2, :f4, :main]
** (ErrInvestmentLost) The money in your account didn't do well
    challengev3.exs:40: BankAcount.main/0
    challengev3.exs:47: (file)
defmodule BankAcount do
def f1(stack) do
number = :rand.uniform(9) + 1
case rem(number, 2) do
0 -> ["ErrBankAccountEmpty", [:f1 | stack]]
_ -> ["ErrInvestmentLost", [:f1 | stack]]
end
end
def f2(stack) do
f1([:f2 | stack])
end
def f3(stack) do
f1([:f3 | stack])
end
def f4(stack) do
number = :rand.uniform(9) + 1
case rem(number, 2) do
0 -> f2([:f4 | stack])
_ -> f3([:f4 | stack])
end
end
def main() do
case f4([:main]) do
[_err, [:f1, :f2, :f4, :main]] -> "Aand it's gone"
[_err, [:f1, :f3, :f4, :main]] -> "The money in your account didn't do well"
_err -> "This line is for bank members only"
end
|> raise
end
end
BankAcount.main()

Alternative 4

This one we pattern match the callstack to determine which error to show.

** (RuntimeError) Aand it's gone
    challengev4.exs:32: BankAcount.main/0
    challengev4.exs:36: (file)
** (RuntimeError) The money in your account didn't do well
   challengev4.exs:32: BankAcount.main/0
   challengev4.exs:36: (file)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment