enso/docs/semantics/wrapped-errors.md
GregoryTravis f2cb1f097e
Support on_problems=Problem_Behavior.Report_Warning and Map_Error wrapping in Vector.map (#8595)
Implements `Warnings.get_all wrap_errors=True` which wraps warnings attached to values inside vectors with `Map_Error`, which includes the position of the value within the vector. See [the documentation](https://github.com/enso-org/enso/blob/develop/docs/semantics/wrapped-errors.md) for more details.

`get_all wrap_errors=True` does not change the warnings that are attached to values -- it wraps them before returning them to the caller, but does not change the original warnings attached to the values.

Wrapped warnings only appear attached to the vector itself. The values inside the vector do not have their warnings wrapped.

Warning propagation is not changed at all; `Warnings.get_all` (with default `wrap_errors=False`) behaves as before. `get_all wrap_errors=True` is meant to be used primarily by the IDE, although it can be used anywhere this wrapping is desired.
2024-01-16 09:36:22 +00:00

4.4 KiB

layout title category tags order
developer-doc Wrapped Errors semantics
semantics
errors
runtime
12

Wrapped Errors

A wrapped error is an error wrapped in an additional 'error wrapper' value providing additional information about the error.

For example, an error attached to a value within a Vector can be wrapped in a Map_Error wrapper which indicates the position of the value within the Vector.

(Such errors are only wrapped when they are obtained through Warning.get_all wrap_errors=True; see Obtaining Wrapped Errors.)

For example:

err = My_Error.Error "my error"
vec = [10, Warning.attach err 20, 30]
IO.println (Warning.get_all vec)
IO.println (Warning.get_all wrap_errors=True vec)

Output:

[My_Error.Error my error]
[Map_Error.Error 1 (My_Error.Error my error)] # The error is at index 1

Catching Wrapped Errors

Wrapped errors are "transparent" to Error.catch. That is, if you attempt to catch a certain error, but the error that is actually thrown is wrapped, the catch will still succeed. In the example below, a My_Error error is thrown in a call to map, and so the resulting error is wrapped. You can catch the error as a My_Error, or as a Map_Error.

fun x = if x == 20 then Error.throw (My_Error.Error "my error") else x
[10, 20, 30].map fun . catch My_Error e->
    IO.println e
[10, 20, 30].map fun . catch Map_Error e->
    IO.println e

Output:

(My_Error.Error 'my error')
(Map_Error.Error 1 (My_Error.Error 'my error'))

Note that if you catch the error as the inner error (My_Error), the Map_Error wrapper is stripped off.

Implementing Error Wrappers

An error wrapper is a regular Enso value that has a conversion to Wrapped_Error. For example:

type Map_Error
    Error (index:Integer) inner_error

Wrapped_Error.from (that : Map_Error) = Wrapped_Error.Value that that.inner_error

The from implementation allows Error.catch to detect that it is an error wrapper, and possibly perform automatic unwrapping on it.

Obtaining Wrapped Errors

Wrapped errors are obtained in two ways:

  • An error thrown during a call to Vector.map
  • An error attached to a value within a Vector

In the case of an error thrown during Vector.map, the error is caught, wrapped in Map_Error, and re-thrown.

In the case of an error attached to a value within a Vector, the wrapper is added by Warning.get_all wrap_errors=True when it is called on the Vector. In this case, the wrapping is not attached to the value itself, and is therefore not propagated to downstream values.

Map_Error

Map_Error is the motivating example for wrapped errors, and is currently the only implemented error wrapper. It exists so that an error thrown during a map call over a large Vector will contain the index at which the error occurred, so it can be displayed to the user in the IDE.

Note that the error does not have to occur during a call to map. A Map_Error wrapping is added by Warning.get_all wrap_errors=True to any error attached to a value within a Vector (or any array-like container). If the value is extracted from the Vector (for example, using .at), its attached Warning is not wrapped.

If a value is nested within multiple Vectors, its attached errors are wrapped with Map_Error multiple times. The outermost Map_Error index indicates the index into the outermost Vector, the second Map_Error index the index into the sub-Vector within the outermost Vector, and so on.

For example:

fun a = if a == 30 then Error.throw (My_Error.Error a) else a+1
nested_vector = [[10, 20, 30, 40], [30, 10, 20, 30]]
result = nested_vector.map (_.map fun on_problems=Problem_Behavior.Report_Warning) on_problems=Problem_Behavior.Report_Warning
warnings = Warning.get_all wrap_errors=True result . map .value
warnings.map w->
    IO.println w

Output:

(Map_Error.Error 1 (Map_Error.Error 3 (My_Error.Error 30))) # [1, 3]
(Map_Error.Error 1 (Map_Error.Error 0 (My_Error.Error 30))) # [1, 0]
(Map_Error.Error 0 (Map_Error.Error 2 (My_Error.Error 30))) # [0, 2]

Testing Wrapped Errors

The following test utilities take an unwrap_errors parameter:

  • Error.should_fail_with
  • Problem.test_problem_handling
  • Problem.expect_warning
  • Problem.expect_only_warning

By default, these methods will automatically unwrap errors. Passing unwrap_errors=False will disable this behavior.