# Use `Integer.parse/1` for params, not `String.to_integer/1`

Phoenix does zero automatic type coercion. Every value in `params` arrives as a string — or not at all. Reaching for `String.to_integer/1` on user-supplied params is a crash waiting to happen in two different ways.

```elixir
# ❌ Crashes on bad input AND on missing keys
def handle_event("paginate", params, socket) do
  page = String.to_integer(params["page"])  # raises ArgumentError on "abc"
                                             # raises FunctionClauseError on nil
  {:noreply, assign(socket, page: page)}
end

# ✅ Handles both gracefully
def handle_event("paginate", params, socket) do
  case parse_integer(params["page"]) do
    {:ok, page} -> {:noreply, assign(socket, page: page)}
    :error -> {:noreply, socket}
  end
end
```

`Plug.Conn.Query` decodes every query parameter as a string. When a key is absent entirely, `params["page"]` returns `nil`, and `String.to_integer(nil)` raises `** (FunctionClauseError) no function clause matching in String.to_integer/1` — a different crash from the `ArgumentError` you'd get on `"abc"`. Both are unhandled exceptions in production.

`Integer.parse/1` is the right tool, but it has a gotcha you need to account for:

```elixir
Integer.parse("123")     # {123, ""}    — success
Integer.parse("-42")     # {-42, ""}    — negative numbers work
Integer.parse("abc")     # :error
Integer.parse("")        # :error
Integer.parse(" 123")    # :error       — leading whitespace fails
Integer.parse("123abc")  # {123, "abc"} — partial parse, NOT :error
```

That last case is the one that bites people. `"123abc"` does not return `:error` — it returns a tuple with a non-empty remainder. Matching only on `{n, ""}` rejects it correctly:

```elixir
@spec parse_integer(String.t() | nil) :: {:ok, integer()} | :error
def parse_integer(value) when is_binary(value) do
  case Integer.parse(value) do
    {n, ""} -> {:ok, n}
    _ -> :error
  end
end

def parse_integer(_), do: :error
```

The second clause catches `nil` and any other non-string value without an explicit nil check. The `_` branch in the `case` handles both `:error` and partial parses like `{123, "abc"}` in one shot — no need to enumerate them separately.

A few things worth keeping in mind:

- **Leading whitespace fails silently** — `Integer.parse(" 123")` returns `:error`, so if you're dealing with form inputs that might have whitespace, `String.trim/1` before parsing is appropriate
- **`String.to_integer/1` is not always wrong** — it's fine when the value is guaranteed to be a well-formed integer string, such as an ID coming from your own database query or a compiled route parameter. It's user-supplied params where it's dangerous
- **`Integer.parse/1` accepts any radix** — `Integer.parse("ff", 16)` returns `{255, ""}`, which is handy for things like color parsing


---

Created by: almirsarajcic
Date: March 18, 2026
URL: https://elixirdrops.net/d/3FNfK2Q6
