Error system

Status: the Rust compiler (rust/, cargo build --release -p sky) is the primary Sky compiler; the Haskell compiler is preserved under legacy-haskell-compiler/. Verified by the example sweep + compiler test suite (cargo test + xtask gates). See ../compiler/versions.md for the changelog.

Since v0.9, every fallible operation in Sky returns a value whose error slot is Sky.Core.Error — a structured ADT with eleven kinds and typed details. There is no more Result String or Task String on any public surface.

The shape

module Sky.Core.Error exposing (..)


type Error
    = Error ErrorKind ErrorInfo


type ErrorKind
    = Io
    | Network
    | Ffi
    | Decode
    | Timeout
    | NotFound
    | PermissionDenied
    | InvalidInput
    | Conflict
    | Unavailable
    | Unexpected


type alias ErrorInfo =
    { message : String
    , details : ErrorDetails
    }


type ErrorDetails
    = NoDetails
    | FfiPanic String
    | TypeMismatch String String
    | HttpStatus Int
    | JsonDecode String
    | Custom (Dict String String)

Constructors

io : String -> Error
network : String -> Error
ffi : String -> Error
decode : String -> Error
timeout : String -> Error
notFound : Error
permissionDenied : Error
invalidInput : String -> Error
conflict : String -> Error
unavailable : String -> Error
unexpected : String -> Error

Plus builders:

withMessage : String -> Error -> Error
withDetails : ErrorDetails -> Error -> Error
toString : Error -> String
isRetryable : Error -> Bool

Usage pattern

import Sky.Core.Error as Error exposing (Error(..), ErrorKind(..))


loadUser : Db -> String -> Task Error (List User)
loadUser db id =
    Db.queryDecode db "SELECT * FROM users WHERE id = ?" [ id ] userDecoder
        |> Task.mapError classifyError


classifyError : Error -> Error
classifyError e =
    case e of
        Error PermissionDenied info ->
            info.message
                |> String.append "user lacks permission: "
                |> Error.invalidInput
        _ ->
            e

Go side

Every runtime kernel that can fail returns rt.SkyResult[any, any] where the Err slot is a value produced by one of the Err* builders in runtime-go/rt/rt.go:

ErrIo(msg string) any
ErrNetwork(msg string) any
ErrFfi(msg string) any
ErrDecode(msg string) any
ErrTimeout(msg string) any
ErrNotFound() any
ErrPermissionDenied() any
ErrInvalidInput(msg string) any
ErrConflict(msg string) any
ErrUnavailable(msg string) any
ErrUnexpected(msg string) any

These produce skyErrorAdt values that match the Sky ADT's runtime layout ({Tag, SkyName, V0 (kind), V1 (info)}), so Sky-side pattern matching works without a translation layer.

Panic recovery

func SkyFfiRecover(out *any) func()
func SkyFfiRecoverT[A any](out *SkyResult[any, A]) func()

Every FFI wrapper defers one of these. On panic, out is set to Err(ErrFfi(<panic-message>)) with an FfiPanic detail carrying the recovered value. Sky code never sees a Go panic.

When to use which kind

KindUse for
IoFile / DB / disk failures
NetworkHTTP / DNS / transport failures
FfiPanics caught at the FFI boundary
DecodeJSON / binary decoder failures
TimeoutDeadlines, context cancellation
NotFoundRecord or resource absent
PermissionDeniedAuth / access-control failures
InvalidInputUser input validation
ConflictOptimistic-lock / unique-constraint failures
UnavailableService overloaded / circuit-broken
UnexpectedFallback when no other kind fits

isRetryable returns True for Timeout, Network, and Unavailable. Use it in retry / backoff policies.

Migration from Result String

If you're upgrading a pre-v1 project:

-- Before:
loadUser : String -> Task String User

-- After:
loadUser : String -> Task Error User

If your error branch was Err e -> Err (Error.io e) and the upstream now returns Error (not String), collapse to Err e -> Err e. Error chaining through combinators preserves the kind.

Forbidden in v0.9+ public surfaces (enforced by Sky.ErrorUnificationSpec):