Error-free Elixir

Error-free Elixir

Error handling can greatly increase the complexity of the system. In Elixir, the default way of reducing the error-handling code is to use the "Let it crash!" approach. But there's another way.

In this talk, you'll learn how to eliminate errors by redefining the operation semantics, all supported by real-life examples. By doing so, you'll be able to not only reduce the need for error handling, but also simplify the entire codebase, make it smaller, more testable and easier to understand.

373dd7c51433dc3c38436dcfdec79cdc?s=128

Maciej Kaszubowski

July 18, 2019
Tweet

Transcript

  1. mkaszubowski94 Maciej Kaszubowski ERROR-FREE ELIXIR

  2. mkaszubowski94 Error handling is crucial

  3. mkaszubowski94 Error handling is a big source of complexity

  4. mkaszubowski94 def cancel_job(user_id, job_id) do Job  Repo.get(job_id)  Job.cancel_changeset(%{user_id:

    user_id, canceled_at: now()})  Repo.update() end
  5. mkaszubowski94 def cancel_job(user_id, job_id) do with {:ok, job}  fetch_job(job_id),

    :ok  can_cancel?(job, user_id) do job  Job.cancel_changeset(%{user_id: user_id, canceled_at: now()})  Repo.update() else nil  {:error, :not_found} {:error, :already_canceled}  {:error, :already_canceled} {:error, :already_completed}  {:error, :already_completed} {:error, :no_permissions}  {:error, :no_permissions} {:error, %Changeset{} = changeset}  {:error, changeset} end end
  6. mkaszubowski94 When designing, we focus mostly on the happy path.

  7. mkaszubowski94 We have to design error-handling too.

  8. mkaszubowski94 Not errors are equal

  9. mkaszubowski94 Users Sources of errors

  10. mkaszubowski94 Users Other services Sources of errors

  11. mkaszubowski94 Users Infrastructure Other services Sources of errors

  12. mkaszubowski94 </> Developers Users Infrastructure Other services Sources of errors

  13. mkaszubowski94 Each requires a different error-handling strategy

  14. Strategies

  15. mkaszubowski94 Let it crash!

  16. mkaszubowski94 Let it crash!

  17. mkaszubowski94 Infrastructure Other services Let it crash! • Incorrect behaviour

    • Bugs hard to reproduce • Bugs hard to recover from What?
  18. mkaszubowski94 Infrastructure Other services Let it crash! • Incorrect behaviour

    • Bugs hard to reproduce • Bugs hard to recover from What? Why? • Protect invariants • Restarting can help
  19. mkaszubowski94 :error tuples

  20. mkaszubowski94 :error tuples

  21. mkaszubowski94 • User/Client errors • Correct behaviour (!) • Predicable

    errors What? Users Other services :error tuples
  22. mkaszubowski94 • User/Client errors • Correct behaviour (!) • Predicable

    errors What? Why? • Client can interpret the error • Client can recover from errors Users Other services :error tuples
  23. mkaszubowski94 </> Developers Users Infrastructure Other services Sources of errors

  24. mkaszubowski94 </> Developers Users Infrastructure Other services Sources of errors

  25. mkaszubowski94 </> Developers Users Infrastructure Other services Sources of errors

  26. mkaszubowski94 </> Developers Users Infrastructure Other services Sources of errors

  27. mkaszubowski94 </> Developers Users Infrastructure Other services Sources of errors

    ?
  28. mkaszubowski94 </> Developers Users Infrastructure Other services Sources of errors

    :(
  29. mkaszubowski94 Controller Context Context.Xxx

  30. mkaszubowski94 Controller Context Context.Xxx

  31. mkaszubowski94 We're too used to the "standard" ways

  32. mkaszubowski94 case some_function(args) do {:ok, something}  #  {:error,

    reason}  {:error, reason} end
  33. mkaszubowski94 Things to consider Can the layer above interpret the

    error? 1 2 How is the error possible? 3 Can the layer above fix the error?
  34. mkaszubowski94 Don't deal with errors. Prevent them.

  35. Eliminating errors

  36. mkaszubowski94 Heuristics Declarative operations 1 2 Reasonable defaults 3 Don't

    fight the real world
  37. mkaszubowski94 Heuristics Declarative operations 1 2 Reasonable defaults 3 Don't

    fight the real world
  38. mkaszubowski94 defmodule Accounts do @spec get_settings(id())  {:ok, t()} |

    {:error, :not_found} def get_settings(user_id) do case Repo.get_by(Settings, user_id: user_id) do %Settings{} = settings  {:ok, settings} nil  {:error, :not_found} end end end
  39. mkaszubowski94 defmodule Accounts do @spec get_settings(id())  {:ok, t()} |

    {:error, :not_found} def get_settings(user_id) do case Repo.get_by(Settings, user_id: user_id) do %Settings{} = settings  {:ok, settings} nil  {:error, :not_found} end end end
  40. mkaszubowski94 defmodule Accounts do @spec get_settings(id())  {:ok, t()} def

    get_settings(user_id) do case Repo.get_by(Settings, user_id: user_id) do %Settings{} = settings  {:ok, settings} nil  {:ok, Settings.default()} end end end
  41. mkaszubowski94 Heuristics Declarative operations 1 2 Reasonable defaults 3 Don't

    fight the real world
  42. mkaszubowski94 def cancel_job(user_id, job_id) do with {:ok, job}  fetch_job(job_id),

    :ok  can_cancel?(job, user_id) do job  Job.cancel_changeset(%{user_id: user_id, canceled_at: now()})  Repo.update() else nil  {:error, :not_found} {:error, :already_canceled}  {:error, :already_canceled} {:error, :already_completed}  {:error, :already_completed} {:error, :no_permissions}  {:error, :no_permissions} {:error, %Changeset{} = changeset}  {:error, changeset} end end
  43. mkaszubowski94 Describe the end goal, not the steps to reach

    it
  44. mkaszubowski94 def cancel_job(user_id, job_id) do with {:ok, job}  fetch_job(job_id),

    :ok  can_cancel?(job, user_id) do job  Job.cancel_changeset(%{user_id: user_id, canceled_at: now()})  Repo.update() else nil  {:error, :not_found} {:error, :already_canceled}  {:error, :already_canceled} {:error, :already_completed}  {:error, :already_completed} {:error, :no_permissions}  {:error, :no_permissions} {:error, %Changeset{} = changeset}  {:error, changeset} end end
  45. mkaszubowski94 def cancel_job(user_id, job_id) do with {:ok, job}  fetch_job(job_id),

    :ok  can_cancel?(job, user_id) do job  Job.cancel_changeset(%{user_id: user_id, canceled_at: now()})  Repo.update() else nil  {:error, :not_found} {:error, :already_canceled}  {:error, :already_canceled} {:error, :already_completed}  {:error, :already_completed} {:error, :no_permissions}  {:error, :no_permissions} {:error, %Changeset{} = changeset}  {:error, changeset} end end
  46. mkaszubowski94 def cancel_job(user_id, job_id) do with {:ok, job}  fetch_job(job_id),

    :ok  can_cancel?(job, user_id) do job  Job.cancel_changeset(%{user_id: user_id, canceled_at: now()})  Repo.update() else nil  {:error, :not_found} {:error, :already_canceled}  {:ok, job} {:error, :already_completed}  {:error, :already_completed} {:error, :no_permissions}  {:error, :no_permissions} {:error, %Changeset{} = changeset}  {:error, changeset} end end
  47. mkaszubowski94 Heuristics Declarative operations 1 2 Reasonable defaults 3 Don't

    fight the real world
  48. mkaszubowski94 def take_off_shelf(product_id, shelf_id) do if product_on_the_shelf?(product_id, shelf_id) do remove_from_shelf!(product_id,

    shelf_id) else {:error, :product_missing} end end
  49. mkaszubowski94 def take_off_shelf(product_id, shelf_id) do if product_on_the_shelf?(product_id, shelf_id) do remove_from_shelf!(product_id,

    shelf_id) else {:error, :product_missing} end end
  50. mkaszubowski94 def take_off_shelf(product_id, shelf_id) do if product_on_the_shelf?(product_id, shelf_id) do remove_from_shelf!(product_id,

    shelf_id) else notify_operators(product_id, shelf_id) end end
  51. mkaszubowski94 We cannot prevent people from making mistakes

  52. mkaszubowski94 Not all errors are bad

  53. mkaszubowski94 Error handling has to be designed

  54. Why talk about this?

  55. mkaszubowski94 Design is not about a single clever idea

  56. mkaszubowski94 We cannot rely on intuition

  57. mkaszubowski94 You will make wrong decisions Make sure you learn

    from that.
  58. mkaszubowski94 A set of clear and simple heuristics and questions

    is really helpful
  59. mkaszubowski94 Approach design more thoughtfully

  60. mkaszubowski94 Use facts instead of opinions

  61. mkaszubowski94 Gather as many questions and heuristics as possible.

  62. mkaszubowski94 Gather as many questions and heuristics as possible. And

    share them!
  63. mkaszubowski94 Share the process of making decisions.

  64. mkaszubowski94 "A Philosophy of Software Design" - John Ousterhout "The

    art of destroying software" - Greg Young Credits
  65. mkaszubowski94 Maciej Kaszubowski THANKS! I hope it was useful