Skuld.Effects.Throw (skuld v0.1.26)

View Source

Throw/Catch effects - error handling with scoped catching.

Uses the %Skuld.Comp.Throw{} struct as the error result type, which is intercepted by catch_error via leave_scope.

Architecture

  • throw(error) returns %Throw{error: error} as the result
  • catch_error installs a leave_scope that intercepts %Throw{} results
  • When caught, the recovery computation runs and continues normal flow
  • Normal completion passes through unchanged
  • If recovery re-throws, the error propagates to outer catch handlers

Summary

Functions

Install Throw handler via catch clause syntax.

Catch errors from a sub-computation.

Default handler - return Throw struct as result (does not call k)

Intercept thrown errors locally within a computation.

Throw an error - does not resume

Catch and return Either-style result.

Install a scoped Throw handler for a computation.

Functions

__handle__(comp, config)

Install Throw handler via catch clause syntax.

Config is ignored (Throw handler takes no configuration):

catch
  Throw -> nil

catch_error(comp, error_handler)

Catch errors from a sub-computation.

If the sub-computation throws, the error handler is invoked and its result continues through normal flow (the continuation chain). This allows catch to fully recover from errors - subsequent binds will receive the recovery value.

If the recovery computation itself throws, that error propagates through the leave_scope chain to any outer catch handlers.

Normal completion passes through unchanged (no wrapping).

Example

# Transparent recovery - catch fully handles the error
Throw.catch_error(
  risky_computation(),
  fn :not_found -> Comp.pure(:default) end
)
# Returns either the value or :default

# Nested catch - inner catches first, unhandled propagates to outer
Throw.catch_error(
  Throw.catch_error(inner, fn :a -> ... end),
  fn :b -> ... end
)

handle(throw, env, k)

Default handler - return Throw struct as result (does not call k)

intercept(comp, handler)

Intercept thrown errors locally within a computation.

This is the IIntercept.intercept/2 implementation for Throw, enabling {Throw, pattern} clauses in comp block catch sections.

Delegates to catch_error/2.

throw(error)

@spec throw(term()) :: Skuld.Comp.Types.computation()

Throw an error - does not resume

try_catch(comp)

Catch and return Either-style result.

Wraps both success and error paths for uniform handling:

  • Success: {:ok, value}
  • Error: {:error, error}

Exception Handling

When exceptions are raised inside computations, they are caught and converted to {:error, unwrapped_value}. The Skuld.Comp.IThrowable protocol determines how exceptions are unwrapped:

  • By default, exceptions are returned as-is (e.g., {:error, %ArgumentError{}})
  • Domain exceptions can implement IThrowable to return cleaner error values

For other exception kinds:

  • :throw values become {:error, {:thrown, value}}
  • :exit reasons become {:error, {:exit, reason}}

Example

result = Throw.try_catch(risky_computation())
case result do
  {:ok, value} -> handle_success(value)
  {:error, %ArgumentError{}} -> handle_bad_input()
  {:error, {:not_found, id}} -> handle_not_found(id)
end

IThrowable Protocol

Implement Skuld.Comp.IThrowable for domain exceptions to get clean error values:

defimpl Skuld.Comp.IThrowable, for: MyApp.NotFoundError do
  def unwrap(%{entity: entity, id: id}), do: {:not_found, entity, id}
end

with_handler(comp)

Install a scoped Throw handler for a computation.

Installs the Throw handler for the duration of comp. The handler is restored/removed when comp completes or throws.

The argument order is pipe-friendly.

Example

# Wrap a computation with Throw handling
comp_with_throw =
  comp do
    result <- risky_operation()
    return(result)
  end
  |> Throw.with_handler()

# Compose with other handlers
my_comp
|> Throw.with_handler()
|> State.with_handler(0)
|> Comp.run(Env.new())