Packages

Error handling utilities with support for error kinds, context, and sources.

Current section

Files

Jump to
glerror src glerror.erl
Raw

src/glerror.erl

-module(glerror).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/glerror.gleam").
-export([inspect_format/1, to_kind/1, string_format/1, to_string/1, pretty_print/1, as_format/1, new/1, error/1, add_context/2, add_context_fn/2, with_source/2, map_kind/2, context/2, lazy_context/2, from_result/3, kind/1]).
-export_type([kind_error/1, kind/1, detail/0, format_args/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(" Error handling utilities with support for error kinds, context, and sources.\n").
-opaque kind_error(DPK) :: {kind_error,
kind(DPK),
list(fun(() -> binary())),
list(fun((format_args()) -> binary()))}.
-type kind(DPL) :: {kind, DPL, fun((format_args()) -> binary())}.
-type detail() :: high | low.
-type format_args() :: {format_args, detail()}.
-file("src/glerror.gleam", 46).
?DOC(" Formats a term using string.inspect.\n").
-spec inspect_format(any()) -> fun((format_args()) -> binary()).
inspect_format(Term) ->
fun(_) -> gleam@string:inspect(Term) end.
-file("src/glerror.gleam", 29).
-spec to_kind(DPQ) -> kind(DPQ).
to_kind(Kind) ->
{kind, Kind, inspect_format(Kind)}.
-file("src/glerror.gleam", 50).
-spec string_format(binary()) -> fun((format_args()) -> binary()).
string_format(S) ->
fun(_) -> S end.
-file("src/glerror.gleam", 297).
?DOC(
" Convert a KindError to a single-line string representation.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import glerror as error\n"
"\n"
" error.error(\"Database connection failed\")\n"
" |> error.add_context(\"loading user\")\n"
" |> error.add_context(\"fetching profile\")\n"
" |> error.to_string\n"
" // \"Database connection failed [loading user, fetching profile]\"\n"
" ```\n"
).
-spec to_string(kind_error(any())) -> binary().
to_string(Err) ->
Base = (erlang:element(3, erlang:element(2, Err)))({format_args, low}),
With_context = case erlang:element(3, Err) of
[] ->
Base;
Context ->
Context_str = begin
_pipe = Context,
_pipe@1 = lists:reverse(_pipe),
_pipe@2 = gleam@list:map(_pipe@1, fun(C) -> C() end),
gleam@string:join(_pipe@2, <<", "/utf8>>)
end,
<<<<<<Base/binary, " ["/utf8>>/binary, Context_str/binary>>/binary,
"]"/utf8>>
end,
case erlang:element(4, Err) of
[] ->
With_context;
Sources ->
Sources_str = begin
_pipe@3 = Sources,
_pipe@4 = gleam@list:map(
_pipe@3,
fun(S) -> S({format_args, low}) end
),
gleam@string:join(_pipe@4, <<", "/utf8>>)
end,
Sources_title = case erlang:length(Sources) of
1 ->
<<"source"/utf8>>;
_ ->
<<"sources"/utf8>>
end,
<<<<<<<<<<With_context/binary, " ("/utf8>>/binary,
Sources_title/binary>>/binary,
": "/utf8>>/binary,
Sources_str/binary>>/binary,
")"/utf8>>
end.
-file("src/glerror.gleam", 407).
-spec source_to_doc(fun((format_args()) -> binary()), boolean()) -> glam@doc:document().
source_to_doc(Source, Is_last) ->
Connector = case Is_last of
true ->
<<"╰─▶ "/utf8>>;
false ->
<<"├─▶ "/utf8>>
end,
Cont_prefix = case Is_last of
true ->
<<" "/utf8>>;
false ->
<<"│ "/utf8>>
end,
case gleam@string:split(Source({format_args, high}), <<"\n"/utf8>>) of
[] ->
glam@doc:from_string(Connector);
[First | Rest] ->
Docs = [glam@doc:from_string(<<Connector/binary, First/binary>>) |
gleam@list:map(
Rest,
fun(Line) ->
glam@doc:from_string(
<<Cont_prefix/binary, Line/binary>>
)
end
)],
_pipe = Docs,
_pipe@1 = glam@doc:join(_pipe, {line, 1}),
glam@doc:force_break(_pipe@1)
end.
-file("src/glerror.gleam", 356).
?DOC(
" Format a KindError with hierarchical pretty printing.\n"
"\n"
" This produces a tree-like output with numbered context sections and\n"
" box-drawing characters for sources, supporting nested errors via\n"
" `as_format`.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import glerror as error\n"
"\n"
" error.error(\"Not found\")\n"
" |> error.add_context(\"fetching user\")\n"
" |> error.add_context(\"loading dashboard\")\n"
" |> error.with_source(fn(_) { \"HTTP 404\" })\n"
" |> error.pretty_print\n"
" ```\n"
"\n"
" Output:\n"
" ```\n"
" error: Not found\n"
" context:\n"
" 0: fetching user\n"
" 1: loading dashboard\n"
" sources:\n"
" ╰─▶ HTTP 404\n"
" ```\n"
).
-spec pretty_print(kind_error(any())) -> binary().
pretty_print(Err) ->
Root_doc = glam@doc:from_string(
<<"error: "/utf8,
((erlang:element(3, erlang:element(2, Err)))({format_args, high}))/binary>>
),
Parts = [Root_doc],
Parts@1 = case erlang:element(3, Err) of
[] ->
Parts;
Context ->
Lines = begin
_pipe = gleam@list:index_map(
Context,
fun(C, I) ->
glam@doc:from_string(
<<<<<<" "/utf8,
(erlang:integer_to_binary(I))/binary>>/binary,
": "/utf8>>/binary,
(C())/binary>>
)
end
),
glam@doc:join(_pipe, {line, 1})
end,
Context_doc = begin
_pipe@1 = [glam@doc:from_string(<<"context:"/utf8>>), Lines],
_pipe@2 = glam@doc:join(_pipe@1, {line, 1}),
glam@doc:force_break(_pipe@2)
end,
lists:append(Parts, [Context_doc])
end,
Parts@2 = case erlang:element(4, Err) of
[] ->
Parts@1;
Sources ->
Source_docs = case Sources of
[] ->
[];
[Single] ->
[source_to_doc(Single, true)];
[First | Rest] ->
Rest_docs = gleam@list:map(
Rest,
fun(S) -> source_to_doc(S, true) end
),
[source_to_doc(First, false) | Rest_docs]
end,
Sources_doc = begin
_pipe@3 = [glam@doc:from_string(<<"sources:"/utf8>>) |
Source_docs],
_pipe@4 = glam@doc:join(_pipe@3, {line, 1}),
glam@doc:force_break(_pipe@4)
end,
lists:append(Parts@1, [Sources_doc])
end,
_pipe@5 = Parts@2,
_pipe@6 = glam@doc:join(_pipe@5, {line, 1}),
_pipe@7 = glam@doc:force_break(_pipe@6),
glam@doc:to_string(_pipe@7, 80).
-file("src/glerror.gleam", 54).
-spec as_format(kind_error(any())) -> fun((format_args()) -> binary()).
as_format(Error) ->
fun(Args) -> case erlang:element(2, Args) of
high ->
pretty_print(Error);
low ->
to_string(Error)
end end.
-file("src/glerror.gleam", 92).
?DOC(
" Create a new error from a custom error kind.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import glerror as error\n"
"\n"
" pub type MyError {\n"
" NotFound\n"
" InvalidInput\n"
" }\n"
"\n"
" let err = error.new(error.to_kind(NotFound))\n"
" ```\n"
).
-spec new(kind(DPV)) -> kind_error(DPV).
new(Kind) ->
{kind_error, Kind, [], []}.
-file("src/glerror.gleam", 104).
?DOC(
" Create a simple string-based error.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import glerror as error\n"
" let err = error.error(\"File not found\")\n"
" ```\n"
).
-spec error(binary()) -> kind_error(binary()).
error(Message) ->
new({kind, Message, string_format(Message)}).
-file("src/glerror.gleam", 123).
?DOC(
" Add a context layer to an error.\n"
"\n"
" Context is added in a stack, with the most recent context appearing first\n"
" when the error is displayed.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import glerror as error\n"
" error.error(\"connection refused\")\n"
" |> error.add_context(\"connecting to database\")\n"
" |> error.add_context(\"initializing application\")\n"
" ```\n"
).
-spec add_context(kind_error(DPY), binary()) -> kind_error(DPY).
add_context(Err, Context) ->
{kind_error,
erlang:element(2, Err),
[fun() -> Context end | erlang:element(3, Err)],
erlang:element(4, Err)}.
-file("src/glerror.gleam", 127).
-spec add_context_fn(kind_error(DQB), fun(() -> binary())) -> kind_error(DQB).
add_context_fn(Err, Context) ->
{kind_error,
erlang:element(2, Err),
[Context | erlang:element(3, Err)],
erlang:element(4, Err)}.
-file("src/glerror.gleam", 155).
?DOC(
" Attach a source error description.\n"
"\n"
" This is useful when you want to include information about the underlying\n"
" cause of the error without exposing the actual error type.\n"
"\n"
" The source is stored as a lazy function and only evaluated when the error\n"
" is displayed, avoiding unnecessary string construction.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import glerror as error\n"
" error.error(\"Database query failed\")\n"
" |> error.with_source(fn() { \"Connection timeout after 30s\" })\n"
"\n"
" // With dynamic data\n"
" error.error(\"Database query failed\")\n"
" |> error.with_source(fn() {\n"
" \"Connection timeout at \" <> format_timestamp(now())\n"
" })\n"
" ```\n"
).
-spec with_source(kind_error(DQE), fun((format_args()) -> binary())) -> kind_error(DQE).
with_source(Err, Source) ->
{kind_error,
erlang:element(2, Err),
erlang:element(3, Err),
lists:append(erlang:element(4, Err), [Source])}.
-file("src/glerror.gleam", 181).
?DOC(
" Transform the error kind using a mapping function.\n"
"\n"
" This is useful when converting between different error types.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import glerror as error\n"
"\n"
" pub type HttpError {\n"
" NotFound\n"
" ServerError\n"
" }\n"
"\n"
" pub type AppError {\n"
" HttpFailed(HttpError)\n"
" ParseFailed\n"
" }\n"
"\n"
" let http_err = error.new(error.to_kind(NotFound))\n"
" let app_err = error.map_kind(http_err, fn(e) { error.to_kind(HttpFailed(e)) })\n"
" ```\n"
).
-spec map_kind(kind_error(DQH), fun((DQH) -> kind(DQJ))) -> kind_error(DQJ).
map_kind(Err, Mapper) ->
{kind_error,
Mapper(erlang:element(2, erlang:element(2, Err))),
erlang:element(3, Err),
erlang:element(4, Err)}.
-file("src/glerror.gleam", 206).
?DOC(
" Add context to a Result if it's an error.\n"
"\n"
" This is the primary way to add context when working with Result types.\n"
" The Ok value passes through unchanged.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" parse_json(data)\n"
" |> glerror.context(\"parsing user data\")\n"
" |> glerror.context(\"loading profile\")\n"
" ```\n"
).
-spec context({ok, DQM} | {error, kind_error(DQN)}, binary()) -> {ok, DQM} |
{error, kind_error(DQN)}.
context(Res, Context) ->
gleam@result:map_error(
Res,
fun(_capture) -> add_context(_capture, Context) end
).
-file("src/glerror.gleam", 228).
?DOC(
" Add context to a Result using a lazy function.\n"
"\n"
" The function is only called if the Result is an Error, avoiding\n"
" unnecessary string construction in the success case.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import glerror as error\n"
"\n"
" fetch_user(user_id)\n"
" |> error.lazy_context(fn() {\n"
" \"fetching user \" <> int.to_string(user_id)\n"
" })\n"
" ```\n"
).
-spec lazy_context({ok, DQU} | {error, kind_error(DQV)}, fun(() -> binary())) -> {ok,
DQU} |
{error, kind_error(DQV)}.
lazy_context(Res, Context) ->
case Res of
{ok, _} ->
Res;
{error, Err} ->
{error, add_context(Err, Context())}
end.
-file("src/glerror.gleam", 251).
?DOC(
" Convert a Result with a different error type to use KindError.\n"
"\n"
" This is useful when integrating with libraries that use their own error types.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" int.parse(\"42\")\n"
" |> error.from_result(\n"
" Kind(\"Invalid integer\", fn(_) { \"Invalid integer\" }),\n"
" fn(_) { fn(_) { \"Parse error\" } }\n"
" )\n"
" ```\n"
).
-spec from_result(
{ok, DRC} | {error, DRD},
kind(DRG),
fun((DRD) -> fun((format_args()) -> binary()))
) -> {ok, DRC} | {error, kind_error(DRG)}.
from_result(Res, Kind, Format_source) ->
case Res of
{ok, Value} ->
{ok, Value};
{error, Err} ->
{error,
begin
_pipe = new(Kind),
with_source(_pipe, Format_source(Err))
end}
end.
-file("src/glerror.gleam", 278).
?DOC(
" Get the error kind from a KindError.\n"
"\n"
" ## Example\n"
"\n"
" ```gleam\n"
" import glerror as error\n"
"\n"
" let err = error.new(error.to_kind(NotFound))\n"
" error.kind(err) // NotFound\n"
" ```\n"
).
-spec kind(kind_error(DRL)) -> DRL.
kind(Err) ->
erlang:element(2, erlang:element(2, Err)).