Dialyzer - Function foo/1 has no local return

Hello everyone,

Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been almost 3 years, and I’m really excited and having so much fun.

Having worked on some statically typed languages I’ve been using typespecs and Dialyzer a lot more in my day to day. And this particular message has been driving me crazy:

Function foo/1 has no local return

I just spent about 4 hours trying to figure out why Dialyzer was so upset. And it turned out it’s because when I want to refactor a type I forget to change it everywhere in the code. Dialyzer starts complaining and I can’t figure out why. So I end up having to pinpoint what’s causing the issue and then fixing it. A lot of the time it’s for something completely unrelated! Like for example using the %{ my_struct | key: val} update syntax to update a struct instead of struct(my_struct, key: val).

Has anybody else run into this issue before? And is there a way to help Dialyzer get better at pinpointing the exact issue?

Cheers!

While I don’t know about any kind of universal solutions you can take a look at this article:
Dialyzer, or how I learned to stop worrying and love the cryptic error messages

There is also one useful hex package:

In case you have no idea what dialyzer is trying to say you can use dialyxir mix task:

mix dialyzer.explain unmatched_return
1 Like

The error has no local return means that dialyzer can not find a path that won’t raise or throw.

This warning is undebuggable without having more insight into the code.

Function foo/1 has no local return means your function is not total - somewhere the code path would result in your function not returning a value.

Whilst these investigations sometimes take time, in my experience, two things happen.

  1. Dialyzer forces you to be honest about your code - when a function calls other functions, if any of those could raise exceptions, you’ll like get a no local return.
  2. The investigation usually encourages some refactoring which results in more robust, more maintainable code.

Finally, in my experience, whether you like it or not Dialyzer is always right.

The example you gave, one could result in a KeyError whereas the other ignores the error:


iex(1)> defmodule K, do: defstruct []
iex(3)> k = struct(K)
iex(4)> struct(k, key: 123)
%K{}
iex(5)> %{ k | key: 123 }
** (KeyError) key :key not found in: %K{}

It does not mean that. It does mean that from dialyzers perspective the mentioned function will always raise. Not that it has some code path that will raise.

Understanding how dialyzer thinks that it will always raise requires an analisis of the function mentioned, as well as its nested functions.

I have experienced quite often that the problem was some NIF replacement function that raised, rather than calling :erlang.nif_error/1,2.

In many other cases it was some situation in which wrongly declared typespecs misleaded dialyzer to make assumptions that were not matching runtime behaviour.

In many other cases it was some situation in which wrongly declared typespecs misleaded dialyzer to make assumptions that were not matching runtime behaviour.

I think this was the case for me the majority of the time.