A critical deconstruction of the official Elixir library guidelines' anti-patterns section, arguing that each of the nine declared anti-patterns has legitimate use cases. The post walks through all nine rules — including avoiding application configuration, compile-time config, exceptions for control flow, macros, and unsupervised processes — and provides concrete engineering scenarios where breaking each rule is not just acceptable but necessary. The core argument is that guidelines are useful defaults for beginners, not universal laws, and that understanding why a rule exists gives developers the authority to break it when trade-offs demand it.
Table of contents
1. Avoid Application Configuration2. Avoid Compile-Time Application Configuration3. Avoid Using Exceptions for Control-Flow4. Avoid Working with Invalid Data5. Avoid Defining Modules Outside Your Namespace6. Avoid use When an import Is Enough7. Avoid Macros8. Avoid Using Processes for Code Organization9. Avoid Spawning Unsupervised ProcessesSummaryQuestions this post answers
When is it acceptable to use Application.get_env in an Elixir library instead of passing explicit options?
Using Application.get_env is justified when you need system-wide defaults across many modules, such as in umbrella projects where the same configuration (e.g., a markdown plugin) must apply across fifteen sub-applications. Passing explicit opts through forty layers of call stacks turns every intermediate module into a passthrough for keys it doesn't care about. Application config lets operators define defaults once while still allowing per-call overrides. Elixir library authors navigating this trade-off share patterns and war stories on daily.dev.
Why would you use compile-time application configuration in Elixir despite the official guidelines warning against it?
Compile-time configuration via module attributes (e.g., @http_client Application.fetch_env!(...)) is justified in two scenarios: hot loops running millions of times per second where runtime Application.get_env/3 lookups add measurable overhead, and macro-based code generation where AST structures must be constructed at compile time. In the latter case, runtime lookup is literally impossible — the AST must exist before the program runs. Developers optimizing Elixir library performance track discussions like this on daily.dev.
When should I use throw/catch instead of {:ok}/{:error} tuples in Elixir?
throw/catch (or a dedicated exception rescued at the top-level boundary) is appropriate in deeply nested recursive algorithms, such as AST traversals or tree parsers fifteen levels deep. Propagating {:error, reason} back through 14 stack frames requires wrapping every recursive step in with blocks, burying the core algorithm under error-plumbing boilerplate. throw unwinds the stack immediately to the public API boundary, which still returns a clean {:error, reason} tuple to callers. Elixir engineers debating control-flow patterns in library internals find relevant discussions on daily.dev.