我如何 read/debug FunctionClauseError?

How do I read/debug FunctionClauseError?

虽然在 SO () 也有类似的问题,但出于某种原因,错误在我的案例中以另一种方式格式化(而且我理解 FunctionClauseError 的一般含义),所以让我再问一次。

这是我遇到的问题:

 {:error, reason} ->
   Logger.error(fn -> ["failed to fetch internal transactions for  >blocks: ", inspect(reason)] end,
     error_count: unique_numbers_count
   )

给出(我添加了换行符)

2022-04-13T18:44:19.749 application=indexer fetcher=internal_transaction count=10 error_count=10 [error] failed to fetch internal transactions for blocks: %FunctionClauseError{args: nil, arity: 1, clauses: nil, function: :finalize, kind: nil, module: EthereumJSONRPC.Geth.Tracer}

很明显 reason

%FunctionClauseError{args: nil, arity: 1, clauses: nil, function: :finalize, kind: nil, module: EthereumJSONRPC.Geth.Tracer}

我想知道这些位是什么意思,它们可以帮助调试吗?

在模块 EthereumJSONRPC.Geth.Tracer 中函数 finalize 被定义为

defp finalize(%{stack: [top], calls: [calls]}) do

传递给它的参数示例(导致错误)是:

%{
  calls: [[], [], [], []],
  depth: 4,
  stack: [
    %{
      "callType" => "staticcall",
      "from" => nil,
      "gas" => 432013,
      "gasUsed" => 21661,
      "input" => "0xefc4cfa.....",
      "output" => "0x",
      "outputLength" => 64,
      "outputOffset" => 4346,
      "to" => "0x0000000000000000000000000000000000000001",
      "traceAddress" => [0, 0, 0],
      "type" => "call",
      "value" => "0x0"
    },
    %{
      "callType" => "staticcall",
      "from" => nil,
      "gas" => 438139,
      "gasUsed" => 1726,
      "input" => "0xefc4c.......",
      "output" => "0x",
      "outputLength" => 64,
      "outputOffset" => 4026,
      "to" => "0x0000000000000000000000000000000000000001",
      "traceAddress" => [0, 0],
      "type" => "call",
      "value" => "0x0"
    },
    %{
      "callType" => "staticcall",
      "from" => nil
      "gas" => 445060,
      "gasUsed" => 2521,
      "input" => "0xefc4......",
      "output" => "0x",
      "outputLength" => 64,
      "outputOffset" => 3706,
      "to" => "0x0000000000000000000000000000000000000001",
      "traceAddress" => [0],
      "type" => "call",
      "value" => "0x0"
    },
    %{
      "callType" => "call",
      "from" => "0x9a66644084108a1bc23a9ccd50d6d63e53098db6",
      "gas" => 460960,
      "gasUsed" => 11500,
      "input" => "0xba2c.........",
      "output" => "0x",
      "to" => "0x841ce48f9446c8e281d3f1444cb859b4a6d0738c",
      "traceAddress" => [],
      "type" => "call",
      "value" => "0x0"
    }
  ],
  trace_address: [0, 0, 0, 0]
}

这对我来说还不错(它同时具有 stackcalls 属性)。我可以从 args: nil, arity: 1, clauses: nil, function: :finalize, kind: nil 位中提取更多内容用于调试吗?如果你指出原因,它也会有所帮助,但我也想了解如何调试它。额外的道具 (depth, trace_address) 会是问题的根源吗?或者我应该在 finalize 函数体内寻找错误原因? (据我了解,没有)

PS 给寻求调试帮助的其他人的注意事项:错误文本前的位

application=indexer fetcher=internal_transaction count=10 error_count=10

可能包含有用的信息:如果您查找 Logger.metadata,您可能会找到类似

的内容
Logger.metadata(fetcher: :internal_transaction)

这可以提示错误的来源。

默认情况下,如果您不使用 try / rescue,将记录一个 FunctionClauseError,其中包含调试它所需的所有信息(值、模式、堆栈跟踪):

> String.downcase(1)
** (FunctionClauseError) no function clause matching in String.downcase/2   

    The following arguments were given to String.downcase/2:

        # 1
        1

        # 2
        :default

...

但是,如果您恢复错误并将其 return 作为一个元组,您将丢失大部分信息:

try do
  String.downcase(1)
rescue
  err -> {:error, err}
end

结果将只包含一个非常准系统的结构:

{:error,
 %FunctionClauseError{
   args: nil,
   arity: 2,
   clauses: nil,
   function: :downcase,
   kind: nil,
   module: String
 }}

您可以直接登录 rescue 子句,其中 __STACKTRACE__ 可用,使用 this official guide:

中解释的方法
try do
  String.downcase(1)
rescue
  err ->
    # format the exception in a readable way
    Exception.format(:error, err, __STACKTRACE__) 
    |> Logger.error()
    
    # return value
    :error
end

话虽这么说,但在 Elixir 中,依赖 try/rescue 并不常见,如上文指南所述。大多数时候你只是让意想不到的事情失败(著名的“让它崩溃”哲学),并使用 :error 元组来处理你实际期望和想要处理的情况。

旁注:@sabiwara 提供的答案有大量有用信息,应标记为正确


这是您遇到错误的原因:

defp finalize(%{stack: [top], calls: [calls]})

这定义了一个函数,接受带有键的映射,包括但不限于:stack:calls,它们的值是只包含一个元素的列表.

您在那里传递了更长的列表,因此出现错误。这可能是您想要做的:

defp finalize(%{stack: top, calls: calls})
  when is_list(top) and is_list(calls)