Error tuples must carry a structured %ErrorMessage{}, not a bare string literal.
{:error, "something went wrong"} gives callers nothing to match on — a string
reason can only be compared byte-for-byte and carries no code or details. The
house convention returns {:error, %ErrorMessage{}} built through the
elixir_error_message constructors.
# BAD — unmatchable, unstructured reason
defmodule MyApp.Users do
def find(nil), do: {:error, "user id is required"}
end
# GOOD — structured error with code, message and details
defmodule MyApp.Users do
def find(nil), do: {:error, ErrorMessage.bad_request("user id is required")}
endOnly literal reasons are flagged. Variables, atoms and structs pass:
{:error, changeset} # passes — reason unknowable statically
{:error, :timeout} # passes by default (:also_flag_atoms)
{:error, %ErrorMessage{}} # passes — already structured
{:error, ErrorMessage.not_found("...")} # passesMatching on an error tuple someone else constructed is fine — only construction is flagged:
case ThirdParty.call() do
{:error, "expired"} -> :retry # passes — a match, not a construction
endTest files are skipped by default (see :excluded_paths) — {:error, "..."}
literals are legitimate fixture data in tests. :excluded_paths is the single
scoping mechanism on purpose: Credo's builtin :files param is deliberately not
defaulted, because it prunes files before run/2 is ever called and would
silently override excluded_paths: [] for consumers re-enabling test-file
flagging.
Known limitations
Two-element tuples of literals carry no position metadata in the Elixir AST, so
the reported line is the nearest enclosing expression that has one — exact for
one-liners and clause bodies, the def line for a construction inside a
multi-line body.
Keyword pairs are indistinguishable from tuple literals in the AST —
[error: "boom"] and [{:error, "boom"}] parse identically — so pairs inside
list and map literals are never flagged.
Reasons built at runtime ("failed: #{inspect(reason)}" interpolation or
"prefix" <> rest concatenation) are not literals and are not flagged.