Automatically print info including stacktrace for uncaught errors in Elixir

141 Views Asked by At

I am currently learning Elixir and setting up a toy REST service example using Plug. Here is the code that I wrote initially. It has a bug on the row that generates a random number. I was able to figure out the issue. However, when the error occurs there's no helpful info to figure out what the problem is.

defmodule HelloWorld.Web do
  use Plug.Router

  plug :match
  plug :dispatch

  get "/random" do
    conn = Plug.Conn.fetch_query_params(conn)
    max = Integer.parse(Map.fetch!(conn.params, "max"))
    r = Enum.random(0..max) # This throws an error
    conn
    |> Plug.Conn.put_resp_content_type("text/plain")
    |> Plug.Conn.send_resp(200, Integer.to_string(r))
  end

  def child_spec(_) do
    Plug.Adapters.Cowboy.child_spec(
      scheme: :http,
      options: [port: 8080],
      plug: __MODULE__)
  end

end

To figure out the problem, I had to modify the original and add a try/rescue block and print out the error info and stacktrace, which gave me a meaningful error message. So I was able to figure out that Integer.parse also returns the remainder.

defmodule HelloWorld.Web do
  require Logger
  use Plug.Router

  plug :match
  plug :dispatch

  get "/random" do
    try do
      conn = Plug.Conn.fetch_query_params(conn)
      max = Integer.parse(Map.fetch!(conn.params, "max"))
      r = Enum.random(0..max)
      conn
      |> Plug.Conn.put_resp_content_type("text/plain")
      |> Plug.Conn.send_resp(200, Integer.to_string(r))
    rescue
      err ->
        Logger.error(Exception.format(:error, err, __STACKTRACE__))
      reraise err, __STACKTRACE__
    end
  end

  def child_spec(_) do
    Plug.Adapters.Cowboy.child_spec(
      scheme: :http,
      options: [port: 8080],
      plug: __MODULE__)
  end

end

Is there any way for Elixir to do this automatically when it encounters an uncaught error just before it terminates the process?

Update: I ran the app as the book suggested by running iex -S mix. When I curl the URL, I see the following error in the console:

17:35:12.218 [error] #PID<0.386.0> running HelloWorld.Web (cowboy_protocol) terminated
Server: localhost:8080 (http)
Request: GET /random?max=100
** (exit) {%Plug.Conn.WrapperError{conn: %Plug.Conn{adapter: {Plug.Adapters.Cowboy.Conn, :...}, assigns: %{}, body_params: %Plug.Conn.Unfetched{aspect: :body_params}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: "localhost", method: "GET", owner: #PID<0.386.0>, params: %{}, path_info: ["random"], path_params: %{}, port: 8080, private: %{plug_route: {"/random", #Function<1.75067752/2 in HelloWorld.Web.do_match/4>}}, query_params: %Plug.Conn.Unfetched{aspect: :query_params}, query_string: "max=100", remote_ip: {127, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{"mime-version", "1.0"}, {"connection", "keep-alive"}, {"extension", "Security/Digest Security/SSL"}, {"host", "localhost:8080"}, {"accept-encoding", "gzip"}, {"accept", "*/*"}, {"content-length", "0"}], request_path: "/random", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}], scheme: :http, script_name: [], secret_key_base: nil, state: :unset, status: nil}, kind: :error, reason: %ArgumentError{message: "ranges (first..last) expect both sides to be integers, got: 0..{100, \"\"}"}, stack: [{Range, :new, 2, [file: 'lib/range.ex', line: 193]}, {HelloWorld.Web, :"-do_match/4-fun-1-", 2, [file: 'lib/hello_world/web.ex', line: 10]}, {HelloWorld.Web, :"-dispatch/2-fun-0-", 4, [file: 'lib/plug/router.ex', line: 246]}, {:telemetry, :span, 3, [file: '/projects/hello_world/deps/telemetry/src/telemetry.erl', line: 321]}, {HelloWorld.Web, :dispatch, 2, [file: 'lib/plug/router.ex', line: 242]}, {HelloWorld.Web, :plug_builder_call, 2, [file: 'lib/hello_world/web.ex', line: 1]}, {Plug.Adapters.Cowboy.Handler, :upgrade, 4, [file: 'lib/plug/cowboy/handler.ex', line: 18]}, {:cowboy_protocol, :execute, 4, [file: '/projects/hello_world/deps/cowboy/src/cowboy_protocol.erl', line: 442]}]}, []}
2

There are 2 best solutions below

0
On

It heavily depends on how do you test it. Generally speaking, errors when the process crashes are dumped out to the default stderr.

This is the excerpt from my iex session testing it.

iex||1 ▶ Mix.install([:plug_cowboy]) 
iex||2 ▶ … # COPY-PASTE OF YOUR CODE
iex||3 ▶ {:ok, pid} = Supervisor.start_link([{Plug.Cowboy, scheme: :http, plug: HelloWorld.Web, options: [port: 4040]}], strategy: :one_for_one)
{:ok, #PID<0.297.0>}
# Go access http://localhost:4040/random/?max=1000
iex||4 ▶ 
13:37:21.346 [error] #PID<0.403.0> running HelloWorld.Web (connection #PID<0.402.0>, stream id 1) terminated
Server: localhost:4040 (http)
Request: GET /random/?max=1000
** (exit) an exception was raised:
    ** (ArgumentError) ranges (first..last) expect both sides to be integers, got: 0..{1000, ""}
        (elixir 1.14.3) lib/range.ex:193: Range.new/2
        iex:11: anonymous fn/2 in HelloWorld.Web.do_match/4
        lib/plug/router.ex:246: anonymous fn/4 in HelloWorld.Web.dispatch/2
        lib/plug/router.ex:242: HelloWorld.Web.dispatch/2
        iex:2: HelloWorld.Web.plug_builder_call/2
        (plug_cowboy 2.6.0) lib/plug/cowboy/handler.ex:11: Plug.Cowboy.Handler.init/2
        (cowboy 2.9.0) 
        …

You also can test the router as shown in the doc Plug.Router, which would also dump the error message.

That said, it’s unclear what suppressed it in your environment, but it should have been printed by default.

0
On

I don't know why you are not seeing the errors in the console, I get similar results to Aleksei. Here's a couple of other pointers.


You can prevent some logic errors at write-time by using elixir-ls and Dialyzer. Here is how your code looks when I open it in my editor:

The warning is a bit cryptic, but it's telling you that max should be an integer, but it's analysed that max will either be an {integer, binary} tuple or :error.


Is there any way for Elixir to do this automatically when it encounters an uncaught error just before it terminates the process?

If I add use Plug.Debugger to your router, loading the page produces this:

enter image description here