Conventions

A handful of patterns recur across gossamer’s API. None are novel — they follow established Gleam ecosystem idioms — but knowing the conventions makes the binding catalog easier to navigate.

Construction shapes

Configurable bindings take one of three shapes, picked by the argument profile and what the underlying type allows:

See gleam_http for the canonical record-builder precedent.

Per-binding typed errors

Result-returning functions declare a typed error type specific to their domain. Pattern match concrete variants rather than inspecting a generic dynamic payload:

use result <- promise.await(subtle.derive_bits(algorithm, key, length))
case result {
  Ok(bits) -> use_bits(bits)
  Error(crypto.AlgorithmNotSupported) -> fall_back()
  Error(crypto.InvalidAccess) -> abort()
  Error(_) -> log_and_continue()
}

The variants mirror the JS spec’s failure modes (NotSupportedError, InvalidAccessError, OperationError, etc.) as named Gleam variants, each type a closed set.

This follows gleam/fetch.FetchError and aligns with the Gleam conventions doc on descriptive errors.

Transit types for JS interop

A few bindings (Uint8Array, ArrayBuffer, Map, Set, Iterator, AsyncIterator) are exposed as JS-native types because some Web APIs return them and consumers occasionally need to send them through to other JS code. They’re transit types — for crossing the boundary, not for working on directly.

Prefer the canonical Gleam type when you control the data flow:

JS-native (transit)Canonical Gleam
Uint8Array / ArrayBufferBitArray
Mapgleam/dict
Setgleam/set
Iteratorgleam_yielder.Yielder
AsyncIteratorgossamer/async_yielder

Each transit binding ships from_* / to_* bridges (uint8_array.to_bit_array, map.from_dict, etc.) so crossing the boundary is one function call. The canonical Gleam type stays the default for everything inside your code; reach for the transit type only when a JS API forces you to.

Pure-functional API

The public Gleam API is pure-functional. Setters return new values; where the underlying JS object is mutable (FormData, for example), it is cloned at the FFI boundary when mutation would otherwise be observable. Gleam code sees value semantics:

let form_a =
  form_data.new()
  |> form_data_extra.append_file("avatar", small_file)
let form_b = form_a |> form_data_extra.append_file("avatar", large_file)
// form_a still has one "avatar" entry; form_b has two

This costs one clone per setter, traded for the consistency of treating gossamer bindings like any other Gleam value. If you specifically need mutation semantics for performance reasons, you can drop down to a custom FFI — but the public Gleam API will never expose observable mutation.

See also

Search Document