Basics
This check is disabled by default.
Learn how to enable it via .credo.exs.
This check has a base priority of low and works with any version of Elixir.
Explanation
Flags Ash bang calls that raise on {:error, _} instead of returning
{:ok, _} | {:error, _} tuples - awkward in code paths that need to
translate Ash errors into HTTP responses, GraphQL payloads, or
job-runner outcomes.
# Flagged
posts = Ash.read!(MyApp.Post)
# Preferred
case Ash.read(MyApp.Post) do
{:ok, posts} -> ...
{:error, error} -> ...
endRequires the host project to be compiled with Ash loaded - same
contract as UseCodeInterface, MissingCodeInterface, etc. When
Ash isn't loadable the check emits one :ash_missing diagnostic
and becomes a no-op.
Two detectors run when the gate passes:
Ash.*!bangs - top-level (Ash.read!,Ash.create!) and nested (Ash.Filter.parse!,Ash.Expr.eval!).Code-interface bangs -
MyApp.Blog.create_post!from a domain'scode_interface, orMyApp.Blog.Post.create_post!from a resource'scode_interface, including calculation interfaces. Confirmed againstAsh.Resource.Info/Ash.Domain.Info- calls to non-Ash modules are silently skipped, as are user modules that aren't yet compiled (they'd be indistinguishable from arbitrary third-party bangs).
Bang-only APIs - bangs that have no non-bang counterpart, like
Ash.stream! or Ash.Seed.seed! - are skipped by default. Probing
module.__info__(:functions) for the trimmed-! name tells us
the suggested replacement wouldn't exist, so a default suggestion
would name a nonexistent function. To surface these calls anyway
under a generic "ensure failures are handled" message, opt in
with flag_bang_only_apis: true. Calls passing stream?: true
to a read code-interface are also silently skipped, because the
non-bang variant rejects streaming.
The "Prefer Mod.fun" suggestion is worded based on the non-bang
counterpart's typespec: tuple-returning APIs (Ash.read,
Ash.create, ...) get the "match on {:ok, _} | {:error, _}"
message, while helpers whose non-bang twin returns something else
(e.g. Ash.Resource.Info.primary_action/2 returns action | nil)
get a conservative "handle the returned value explicitly" message.
Use excluded_functions to silence specific bangs by
{module, :fun!} tuple:
excluded_functions: [
{MyApp.Blog, :archive_all_posts!}
]Test directories are excluded by default since bang versions in tests
are idiomatic ("crash loudly on unexpected errors"). Override
excluded_paths (e.g. to []) if you want to flag bang calls in
tests too. Entries can be path segments ("test" excludes any file
under a test/ directory) or full file paths
("priv/seeds.exs" excludes that exact file).
Both detectors resolve aliases lexically. The common
alias __MODULE__.Foo pattern is resolved using the enclosing
defmodule, so Foo.archive!() and MyApp.Blog.Foo.archive!()
are treated identically. Unsupported call shapes that can never be
flagged: apply/3, variable modules (mod.fun!()), macro-generated
bang names, and bare __MODULE__.fun!() (no alias).
Check-Specific Parameters
Use the following parameters to configure this check:
:excluded_functions
Bang functions to allow without flagging, given as {module, :fun!} tuples. Defaults to [] - bang-only APIs (those with no non-bang counterpart) are detected dynamically via module.__info__(:functions), so no curated allowlist is needed.
This parameter defaults to [].
:excluded_paths
Paths or regexes to skip. Defaults to test directories, where bang versions are idiomatic.
This parameter defaults to [~r/\/test\//, "test"].
:flag_bang_only_apis
When true, also flag bangs that have no non-bang counterpart (e.g. Ash.stream!, Ash.Seed.seed!) with a generic 'ensure failures are handled' message. Defaults to false because the suggested non-bang twin doesn't exist for these calls; opt in only if your team policy is 'no bare bang calls'.
This parameter defaults to false.
General Parameters
Like with all checks, general params can be applied.
Parameters can be configured via the .credo.exs config file.