narrow-bare-rescue
Narrow bare rescue in Elixir so real errors like KeyError and typos propagate instead of being swallowed. Use to audit rescues and refactor error handling.
Install
npx skills add https://github.com/oliver-kriska/claude-elixir-phoenix/tree/main/plugins/elixir-phoenix/skills/narrow-bare-rescue
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install oliver-kriska-claude-elixir-phoenix@llmmart
git clone https://github.com/oliver-kriska/claude-elixir-phoenix.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole oliver-kriska/claude-elixir-phoenix collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Narrow Bare Rescue
Turn rescue _ -> fallback into rescue _ in [ExceptionType1, ExceptionType2] -> fallback
so programmer bugs propagate while known failure modes stay handled.
Why this matters
Bare rescues (rescue _ ->, rescue e -> — any form without an in clause) swallow
every exception, including UndefinedFunctionError from typos, KeyError from
misspelled map keys, and CompileError from bad HEEx templates. The symptom isn't a stack
trace — it's a silent {:error, :generic} or a nil fallback. Bugs that should surface
in tests or error reporters become quiet degradations.
The Erlang Secure Coding Guide makes
the same case at the BEAM level — rule LNG-002 ("Do Not Use catch") warns that the
legacy catch-all form conflates normal returns, throws, and errors. Bare rescue in Elixir
is the direct analogue.
Iron Laws
- Never leave
rescue _ ->orrescue e ->without aninclause. Every rescue must list exact exception types. The Credo check enforces this after cleanup lands. - Cover every exception the code path can actually raise. Narrowing that drops a real exception is a behavioral regression — trace each call in the body before committing.
- Never include programmer-bug exceptions in the list.
UndefinedFunctionError,CompileError,BadFunctionError, andBadArityErrormust propagate. - Use
reraise e, __STACKTRACE__, neverreraise e, []. Preserve the original stack trace so Oban retry metadata and error reporters show the real origin. - Run
mix compile --warnings-as-errorsbefore committing. Typos in exception module names only surface at compile time — the code looks fine until it loads.
The core transform
# Before — masks programmer bugs
def parse(body) do
Jason.decode!(body)
rescue
_ -> %{}
end
# After — catches only what can actually fail here
def parse(body) do
Jason.decode!(body)
rescue
_ in [Jason.DecodeError, ArgumentError] -> %{}
end
Applies identically to try … rescue … and to function-body def … rescue ….
Workflow
The skill operates in three modes depending on scope:
- Single file —
/narrow-bare-rescue path/to/file.ex - Directory —
/narrow-bare-rescue lib/my_app/util/ - Whole project —
/narrow-bare-rescue --all
Whatever the scope, follow this sequence.
Step 1 — Find the sites
grep -rn "^\s*rescue\s*$" <scope> | head -200
For each hit, read the 3 lines after to classify:
rescue _ ->orrescue var ->— bare, needs narrowingrescue _ in [...] ->orrescue var in Something ->— already typed, skiprescue ExceptionType ->(no variable binding) — already typed, skip
Step 2 — Determine the exception set for each bare site
Read the try / def body and trace what each call can raise. Don't guess from the
function name — verify. Consult order:
Check
${CLAUDE_SKILL_DIR}/references/taxonomy.mdfor the work type (JSON, Ecto, Money, HTTP, etc.). Most sites map cleanly to one row.Grep deps for
defexceptionwhen a specific library isn't in the taxonomy:grep -rn "defexception" deps/<libname>/lib/ | head -10Check
raisecalls in the code path itself — if the body explicitly raisesRuntimeError, include it.
Priorities: cover everything the code can actually raise, exclude programmer-bug
exceptions (see Iron Law #3), and prefer specific types (Jason.DecodeError beats
ArgumentError if both could apply).
Step 3 — Apply the narrowing
For files with ≥3 rescues sharing a taxonomy, hoist to a module attribute — see
${CLAUDE_SKILL_DIR}/references/patterns.md for the module-attribute pattern, Oban reraise, ExCmd exit
errors, and is_exception/1 replacements.
Step 4 — Verify
After changes in each file (or cluster of files), run:
mix compile --warnings-as-errors
mix format <files_changed>
mix test <test_files_for_affected_modules>
The compile step catches typos in exception module names — a real risk since you're writing module names from memory.
Scope
This skill narrows bare rescue clauses. It does not:
- Auto-narrow blindly — behavior preservation matters; trace each call path first
- Touch rescues that are already typed (
rescue e in [X] ->) — those are correct - Cover
catchclauses — throws and exits from the process are a separate concern - Replace
try/rescuewithwithor error-tuple plumbing — that's a larger refactor
References
${CLAUDE_SKILL_DIR}/references/taxonomy.md— verified exception types per work category, plus library-specific gotchas (NimbleCSV, Plug, Phoenix LiveView tokenizer)${CLAUDE_SKILL_DIR}/references/patterns.md— special patterns:is_exception/1, Oban reraise, ExCmd exit errors, module-attribute hoisting, partitioning large cleanups, the regression-prevention Credo check- Erlang Secure Coding Guide — LNG-002: Do Not Use
catch— BEAM-level rationale for preferring narrowtry ... catch/try ... rescueover the legacy catch-all form
Files (claude-elixir-phoenix)
-
references
-
patterns.md 3.8 KB
# Special Rescue Patterns Patterns that come up repeatedly when narrowing bare rescues. Each one is the right shape for a specific callsite, not a generic recipe. ## `is_exception/1` replaces try/rescue around `Exception.message/1` `Exception.message/1` only works on exception structs, so a common defensive pattern is: ```elixir # Before — try/rescue just to handle non-exceptions message = try do Exception.message(reason) rescue _ -> inspect(reason) end # After — guard replaces the rescue message = if is_exception(reason), do: Exception.message(reason), else: inspect(reason) ``` The `is_exception/1` guard (available since Elixir 1.11) is strictly better: it's a compile-time guard, generates no exception, and removes the bare rescue entirely. ## Oban worker "log and reraise" pattern Workers often catch exceptions only to log them, then reraise so Oban's retry machinery still fires. Narrow the set even here — programmer bugs then bypass the misleading log line: ```elixir def perform(%Oban.Job{args: args}) do do_work(args) rescue e in [Req.TransportError, Ecto.ConstraintError, Postgrex.Error] -> Logger.error("worker failed: #{Exception.message(e)}") reraise e, __STACKTRACE__ end ``` Always use `reraise e, __STACKTRACE__` (not `reraise e, []`) to preserve the original stack trace so Oban's retry metadata and error reporters show the right origin. ## ExCmd streams raise a specific exit error `ExCmd.stream!/1` and `ExCmd.stream/1` raise `ExCmd.Stream.AbnormalExit` on non-zero exit. Every ExCmd rescue must include it — it's not caught by `ErlangError` or `RuntimeError`: ```elixir rescue _ in [ExCmd.Stream.AbnormalExit, ErlangError, ArgumentError, MatchError, RuntimeError] -> {:error, :extraction_failed} end ``` ## Module attribute for ≥3 rescues sharing a taxonomy When a file has three or more rescues that all catch the same set, hoist to a module attribute at the top. This prevents drift between clauses and makes the set easy to audit. ```elixir @rescuable_errors [ RuntimeError, ArgumentError, MatchError, FunctionClauseError, KeyError, Ecto.NoResultsError, Ecto.StaleEntryError, Postgrex.Error, DBConnection.ConnectionError, Jason.DecodeError, ExAws.Error ] defp run_tool(tool, args) do tool.call(args) rescue e in @rescuable_errors -> Logger.warning("#{tool} failed: #{Exception.message(e)}") {:error, :tool_failed} end ``` Give the attribute a name that reflects its scope (`@tool_rescuable_errors`, `@metrics_rescuable_errors`, `@form_atom_rescuable_errors`) so different taxonomies in the same module stay distinguishable. ## Scale: partitioning large cleanups When a codebase has ≥50 bare rescue sites, don't send one PR with 50 files changed. Split by directory into 3-7 clusters, one PR per cluster. Each cluster PR runs `mix test` independently and can land on its own schedule. Typical partition boundaries (adjust to the codebase layout): - `lib/<app>/util/` + `lib/<app>/workers/` - `lib/<app>/ai/` + `lib/<app>/email_sync/` - `lib/<app>/` remaining long tail - `lib/<app>_web/` - `lib/mix/tasks/` This keeps each PR under ~200 lines changed and each reviewable on its own. ## Preventing regressions — the Credo check After a cleanup pass lands, add a custom Credo check to prevent new bare rescues from re-entering the codebase. The check should flag `rescue _ ->` and `rescue var ->` patterns and pass `rescue _ in [...] ->` and `rescue var in ExceptionType ->` patterns. Ship the check **disabled** in the same PR that introduces it: ```elixir # .credo.exs {MyApp.Credo.Check.Warning.NoBareRescue, false} ``` Flip it to `[]` in a followup once the cleanup clusters have all merged and CI runs clean. Shipping disabled first means the check module is loaded and its tests run in CI, but it doesn't fail any pre-existing bare rescue that the cleanup hasn't reached yet. -
taxonomy.md 10.4 KB
# Exception Taxonomy for Rescue Narrowing Verified exception types by work category. Use these as the default sets when narrowing bare rescues, and add to them only when you can point to a specific call in the rescue body that raises the extra type. **Last validated:** 2026-04 against Elixir 1.19 / OTP 28. Libraries occasionally rename exception modules between minor versions — if you're narrowing against a newer dep, confirm the names still exist before committing or you'll hit `(CompileError) module is not available`. All entries were validated against the deps of a production Phoenix codebase during a rescue-narrowing audit. When a library updates, re-verify by running: ```bash grep -rn "defexception" deps/<libname>/lib/ ``` ## Table of contents 1. [JSON encode/decode](#json-encodedecode) 2. [Ecto + Postgres](#ecto--postgres) 3. [Money / Decimal arithmetic](#money--decimal-arithmetic) 4. [File I/O](#file-io) 5. [Req HTTP client](#req-http-client) 6. [ExAws (S3 / SES / etc.)](#exaws-s3--ses--etc) 7. [ExCmd shell subprocess](#excmd-shell-subprocess) 8. [Regex](#regex) 9. [Atoms from strings](#atoms-from-strings) 10. [Phoenix forms + atom conversion](#phoenix-forms--atom-conversion) 11. [Plug request parsing](#plug-request-parsing) 12. [Phoenix LiveView HEEx / MDEx](#phoenix-liveview-heex--mdex) 13. [NimbleCSV](#nimblecsv) 14. [Redlines / DOCX / PDF](#redlines--docx--pdf) 15. [Explicit `raise` in your own code](#explicit-raise-in-your-own-code) 16. [Programmer-bug exceptions to EXCLUDE](#programmer-bug-exceptions-to-exclude) --- ## JSON encode/decode ```elixir [Jason.DecodeError, Jason.EncodeError, ArgumentError] ``` - `Jason.decode!/1` → `Jason.DecodeError` on bad JSON, `ArgumentError` on non-binary input - `Jason.encode!/1` → `Jason.EncodeError` on non-encodable term, `Protocol.UndefinedError` for struct without impl - For Elixir 1.18+ `JSON` (built-in, replaces Jason in new code): same exception types via the same modules — the `JSON` module re-exports Jason internals Add `Protocol.UndefinedError` when encoding arbitrary structs: ```elixir [Jason.EncodeError, Protocol.UndefinedError, ArgumentError] ``` ## Ecto + Postgres ```elixir [ Ecto.NoResultsError, Ecto.InvalidChangesetError, Ecto.ConstraintError, Ecto.StaleEntryError, Ecto.Query.CastError, Postgrex.Error, DBConnection.ConnectionError ] ``` - `Repo.get!/2`, `Repo.one!/1` → `Ecto.NoResultsError` - `Repo.insert!/1`, `Repo.update!/1` → `Ecto.InvalidChangesetError` - Unique/foreign key violations → `Ecto.ConstraintError` - Optimistic lock conflicts → `Ecto.StaleEntryError` - Parameter type mismatch in queries → `Ecto.Query.CastError` - Raw Postgres errors (triggers, serialization) → `Postgrex.Error` - Connection drops or pool exhaustion → `DBConnection.ConnectionError` When doing bulk inserts/updates, add `ArgumentError` (for row validation failures in `insert_all`). ## Money / Decimal arithmetic ```elixir [ ArgumentError, ArithmeticError, FunctionClauseError, Decimal.Error, Money.UnknownCurrencyError, Money.InvalidAmountError ] ``` - `Decimal.new/1` on a malformed binary → `Decimal.Error` - `Decimal.div/2` by zero → `ArithmeticError` - `Money.new/2` with unknown currency code → `Money.UnknownCurrencyError` - `Money.new/2` with non-numeric amount → `Money.InvalidAmountError` - Arithmetic on a `nil` that wasn't expected → `ArithmeticError` Add `Protocol.UndefinedError` when piping into `Decimal.cmp/2` with a term that isn't `Decimal`-comparable. ## File I/O ```elixir [File.Error, File.CopyError, ErlangError, ArgumentError] ``` - `File.read!/1`, `File.write!/2` → `File.Error` - `File.cp!/2`, `File.rename!/2` → `File.CopyError` - Bad path / encoding issues → `ErlangError` or `ArgumentError` For atomic temp-file writes (`File.write!` then `File.rename!`), include both. ## Req HTTP client ```elixir [Req.TransportError, Req.HTTPError, Req.DecompressError, ArgumentError] ``` - Network failures (DNS, TCP, TLS) → `Req.TransportError` - HTTP protocol violations → `Req.HTTPError` - Gzip/brotli decompression failures → `Req.DecompressError` - Bad URL → `ArgumentError` (from `URI.parse!/1` inside Req) - `URI.Error` from `URI.parse!/1` directly — add when you call `URI.parse!/1` yourself For `Req.request!/1` with `retries: :safe_transient`, still include the transport error — the `!` variant still raises on final failure. ## ExAws (S3 / SES / etc.) ```elixir [ExAws.Error, MatchError, ArgumentError, ErlangError, Jason.DecodeError] ``` - AWS API errors → `ExAws.Error` - Pattern match on `{:ok, _}` failing when the API returns `{:error, _}` → `MatchError` - Response body JSON parse failure → `Jason.DecodeError` ## ExCmd shell subprocess ```elixir [ExCmd.Stream.AbnormalExit, ErlangError, ArgumentError, MatchError, RuntimeError] ``` - Non-zero exit status from the subprocess → `ExCmd.Stream.AbnormalExit` (**required** in every ExCmd rescue — not caught by any other type) - Port / OS errors → `ErlangError` - Malformed command args → `ArgumentError` Add `ArithmeticError` if the rescue body performs ratio/percentage math on bytes read. ## Regex ```elixir # Add to any rescue where the body uses Regex.compile*, Regex.run, Regex.replace, or ~r sigils: [Regex.CompileError] ``` - `Regex.compile!/1` on malformed pattern → `Regex.CompileError` - `~r/.../` compiled at runtime with user input — same - `Regex.run/2`, `Regex.replace/3` do not raise on non-match, but can raise if given an invalid pre-compiled regex ## Atoms from strings ```elixir [ArgumentError] ``` - `String.to_existing_atom/1` on unknown atom → `ArgumentError` - `String.to_atom/1` is atom-exhaustion dangerous; prefer `to_existing_atom` and catch the ArgumentError Never write `rescue _ ->` around `String.to_existing_atom` — the ArgumentError on unknown atom is often a legitimate signal (e.g., untrusted input), not an unexpected condition. ## Phoenix forms + atom conversion ```elixir [ArgumentError, FunctionClauseError, KeyError] ``` Pattern: ```elixir case Form.input_value(form, :structure_type) do val when is_atom(val) -> val val when is_binary(val) and val != "" -> String.to_existing_atom(val) _ -> nil end rescue _ in [ArgumentError, FunctionClauseError, KeyError] -> nil end ``` - `ArgumentError` → `to_existing_atom` on unknown value - `KeyError` → form missing the field entirely - `FunctionClauseError` → unexpected form shape ## Plug request parsing ```elixir [ ArgumentError, MatchError, FunctionClauseError, CaseClauseError, ErlangError, Plug.BadRequestError, Plug.Conn.InvalidQueryError, Plug.Parsers.ParseError, Plug.Parsers.BadEncodingError ] ``` - `Plug.Conn.Query.decode/1` on invalid UTF-8 → `Plug.Conn.InvalidQueryError` (**not just ArgumentError**) - Multipart parse failures → `Plug.Parsers.ParseError` - Bad Content-Transfer-Encoding → `Plug.Parsers.BadEncodingError` - Too-large body → `Plug.BadRequestError` ## Phoenix LiveView HEEx / MDEx ```elixir [ MatchError, ArgumentError, RuntimeError, FunctionClauseError, Phoenix.LiveView.Tokenizer.ParseError, MDEx.DecodeError, MDEx.InvalidInputError, Regex.CompileError ] ``` **Gotcha:** the HEEx tokenizer error lives at `Phoenix.LiveView.Tokenizer.ParseError`, **not** `Phoenix.LiveView.HTMLTokenizer.ParseError`. The latter is a common misremember — no such module exists. - `MDEx.to_heex!/2` on malformed markdown → `MDEx.DecodeError` - `MDEx.to_html/1` with bad input → `MDEx.InvalidInputError` - HEEx template failing to parse after MDEx output → `Phoenix.LiveView.Tokenizer.ParseError` ## NimbleCSV ```elixir [NimbleCSV.ParseError, MatchError, ArgumentError] ``` **Gotcha:** only `NimbleCSV.ParseError` is public. The RFC4180-specific structs (`NimbleCSV.RFC4180.RowLengthError`, etc.) are **internal** and will fail to compile if referenced. Verified by grepping deps: ```bash grep -rn "defexception" deps/nimble_csv/lib/ # Output: deps/nimble_csv/lib/nimble_csv.ex: defexception [:line, :reason] ``` ## Redlines / DOCX / PDF For `read_word_document.ex`-style code paths: ```elixir # Extracting text [ErlangError, ArgumentError] # Unzipping / copying temp files [File.Error, File.CopyError, ErlangError, ArgumentError, RuntimeError] # Processing text with regex [ArgumentError, Regex.CompileError, FunctionClauseError, KeyError] ``` For `pdftotext_mapper.ex` (ExCmd-based PDF parsing): ```elixir [ExCmd.Stream.AbnormalExit, ErlangError, ArgumentError, MatchError, RuntimeError, ArithmeticError] ``` The `ArithmeticError` is for bounding-box ratio calculations that can divide by zero on malformed PDFs. ## Explicit `raise` in your own code If the `try` body calls functions that explicitly `raise RuntimeError` or `raise "msg"` (which creates a `RuntimeError`), add `RuntimeError` to the rescue list. ```elixir defp validate!(activity) do if problem?(activity), do: raise("Migration halted: #{inspect(activity)}") end def migrate(activities) do Enum.each(activities, &validate!/1) rescue e in [ Ecto.ConstraintError, Ecto.StaleEntryError, Postgrex.Error, DBConnection.ConnectionError, ArgumentError, MatchError, RuntimeError # <-- required because validate! explicitly raises ] -> {:error, e} end ``` **Forgetting `RuntimeError` here is a common mistake** — the old bare rescue was catching it, and the narrowed version silently changes behavior. Always check for `raise` calls in the call path. ## Programmer-bug exceptions to EXCLUDE These should almost never appear in a narrowed rescue list, because catching them is precisely what hides the bugs this skill is meant to surface: | Exception | Why to exclude | |-----------|---------------| | `UndefinedFunctionError` | Typo in a module/function name; must propagate | | `CompileError` | Bad template or macro expansion; must propagate | | `BadFunctionError` | Calling a non-function; must propagate | | `BadArityError` | Wrong number of args at a fun call; must propagate | If a narrowed rescue list includes any of these, re-examine whether the original bare rescue was genuinely meant to suppress them (rare) or was accidentally hiding bugs (common). **Exceptions to the exclusion:** `FunctionClauseError` and `ArgumentError` have legitimate non-bug uses (e.g., `:erlang.binary_to_existing_atom/1` raises `ArgumentError` on unknown input, which is legitimate data). Include them only when you can point to a call in the body that raises them as a data signal, not as a programmer error.
-
-
SKILL.md 5.5 KB
--- name: narrow-bare-rescue description: "Narrow bare rescue in Elixir so real errors like KeyError and typos propagate instead of being swallowed. Use to audit rescues and refactor error handling." effort: medium user-invocable: true argument-hint: "[file_path | directory | --all]" paths: - "**/*.ex" - "**/*.exs" --- # Narrow Bare Rescue Turn `rescue _ -> fallback` into `rescue _ in [ExceptionType1, ExceptionType2] -> fallback` so programmer bugs propagate while known failure modes stay handled. ## Why this matters Bare rescues (`rescue _ ->`, `rescue e ->` — any form without an `in` clause) swallow **every** exception, including `UndefinedFunctionError` from typos, `KeyError` from misspelled map keys, and `CompileError` from bad HEEx templates. The symptom isn't a stack trace — it's a silent `{:error, :generic}` or a `nil` fallback. Bugs that should surface in tests or error reporters become quiet degradations. The Erlang [Secure Coding Guide](https://www.erlang.org/doc/system/secure_coding.html) makes the same case at the BEAM level — rule **LNG-002** ("Do Not Use `catch`") warns that the legacy catch-all form conflates normal returns, throws, and errors. Bare `rescue` in Elixir is the direct analogue. ## Iron Laws 1. **Never leave `rescue _ ->` or `rescue e ->` without an `in` clause.** Every rescue must list exact exception types. The Credo check enforces this after cleanup lands. 2. **Cover every exception the code path can actually raise.** Narrowing that drops a real exception is a behavioral regression — trace each call in the body before committing. 3. **Never include programmer-bug exceptions in the list.** `UndefinedFunctionError`, `CompileError`, `BadFunctionError`, and `BadArityError` must propagate. 4. **Use `reraise e, __STACKTRACE__`, never `reraise e, []`.** Preserve the original stack trace so Oban retry metadata and error reporters show the real origin. 5. **Run `mix compile --warnings-as-errors` before committing.** Typos in exception module names only surface at compile time — the code looks fine until it loads. ## The core transform ```elixir # Before — masks programmer bugs def parse(body) do Jason.decode!(body) rescue _ -> %{} end # After — catches only what can actually fail here def parse(body) do Jason.decode!(body) rescue _ in [Jason.DecodeError, ArgumentError] -> %{} end ``` Applies identically to `try … rescue …` and to function-body `def … rescue …`. ## Workflow The skill operates in three modes depending on scope: 1. **Single file** — `/narrow-bare-rescue path/to/file.ex` 2. **Directory** — `/narrow-bare-rescue lib/my_app/util/` 3. **Whole project** — `/narrow-bare-rescue --all` Whatever the scope, follow this sequence. ### Step 1 — Find the sites ```bash grep -rn "^\s*rescue\s*$" <scope> | head -200 ``` For each hit, read the 3 lines after to classify: - `rescue _ ->` or `rescue var ->` — bare, needs narrowing - `rescue _ in [...] ->` or `rescue var in Something ->` — already typed, skip - `rescue ExceptionType ->` (no variable binding) — already typed, skip ### Step 2 — Determine the exception set for each bare site Read the `try` / `def` body and trace what each call can raise. Don't guess from the function name — verify. Consult order: 1. **Check `${CLAUDE_SKILL_DIR}/references/taxonomy.md`** for the work type (JSON, Ecto, Money, HTTP, etc.). Most sites map cleanly to one row. 2. **Grep deps for `defexception`** when a specific library isn't in the taxonomy: ```bash grep -rn "defexception" deps/<libname>/lib/ | head -10 ``` 3. **Check `raise` calls in the code path itself** — if the body explicitly raises `RuntimeError`, include it. Priorities: cover everything the code can actually raise, exclude programmer-bug exceptions (see Iron Law #3), and prefer specific types (`Jason.DecodeError` beats `ArgumentError` if both could apply). ### Step 3 — Apply the narrowing For files with ≥3 rescues sharing a taxonomy, hoist to a module attribute — see `${CLAUDE_SKILL_DIR}/references/patterns.md` for the module-attribute pattern, Oban reraise, ExCmd exit errors, and `is_exception/1` replacements. ### Step 4 — Verify After changes in each file (or cluster of files), run: ```bash mix compile --warnings-as-errors mix format <files_changed> mix test <test_files_for_affected_modules> ``` The compile step catches typos in exception module names — a real risk since you're writing module names from memory. ## Scope This skill narrows bare `rescue` clauses. It does not: - Auto-narrow blindly — behavior preservation matters; trace each call path first - Touch rescues that are already typed (`rescue e in [X] ->`) — those are correct - Cover `catch` clauses — throws and exits from the process are a separate concern - Replace `try/rescue` with `with` or error-tuple plumbing — that's a larger refactor ## References - `${CLAUDE_SKILL_DIR}/references/taxonomy.md` — verified exception types per work category, plus library-specific gotchas (NimbleCSV, Plug, Phoenix LiveView tokenizer) - `${CLAUDE_SKILL_DIR}/references/patterns.md` — special patterns: `is_exception/1`, Oban reraise, ExCmd exit errors, module-attribute hoisting, partitioning large cleanups, the regression-prevention Credo check - [Erlang Secure Coding Guide — LNG-002: Do Not Use `catch`](https://www.erlang.org/doc/system/secure_coding.html) — BEAM-level rationale for preferring narrow `try ... catch` / `try ... rescue` over the legacy catch-all form
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.