Skip to content

Commit

Permalink
Allow filtering caught error type in Error.catch (#3574)
Browse files Browse the repository at this point in the history
More and more often I need a way to only recover a specific type of a dataflow error (in a similar manner as with panics). So the API for `Error.catch` has been amended to more closely resemble `Panic.catch`, allowing to handle only specific types of dataflow errors, passing others through unchanged. The default is `Any`, meaning all errors are caught by default, and the behaviour of `x.catch` remains unchanged.
  • Loading branch information
radeusgd authored Jul 11, 2022
1 parent ec5cd01 commit 28513a3
Show file tree
Hide file tree
Showing 38 changed files with 194 additions and 136 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
- [Added `File_Format.Excel` support to `Table.write` for new files.][3551]
- [Added append support for `File_Format.Excel`.][3558]
- [Added support for custom encodings in `File_Format.Delimited` writing.][3564]
- [Allow filtering caught error type in `Error.catch`.][3574]

[debug-shortcuts]:
https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug
Expand Down Expand Up @@ -237,6 +238,7 @@
[3552]: https://github.com/enso-org/enso/pull/3552
[3558]: https://github.com/enso-org/enso/pull/3558
[3564]: https://github.com/enso-org/enso/pull/3564
[3574]: https://github.com/enso-org/enso/pull/3574

#### Enso Compiler

Expand Down
36 changes: 26 additions & 10 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Any.enso
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from Standard.Base import all

from Standard.Base.Error.Common import dataflow_error_handler

# The type that subsumes all types.
type Any

Expand All @@ -18,7 +20,7 @@ type Any
Arguments:
- handler: The function to call on this if it is an error value.
catch_primitive : (Error -> Any) -> Any
catch_primitive handler = @Builtin_Method "Any.catch"
catch_primitive handler = @Builtin_Method "Any.catch_primitive"

## Generic conversion of an arbitrary Enso value to a corresponding textual
representation.
Expand Down Expand Up @@ -246,23 +248,37 @@ type Any
if_nothing : Any -> Any
if_nothing ~_ = self

## Executes the provided handler on an error, or returns a non-error value
unchanged.
## Executes the provided handler on an error, or returns the value unchanged.

Arguments:
- handler: The function to call on this if it is an error value. By default
this is identity.
- error_type: The type of error to handle. Defaults to `Any` to handle
all errors.
- handler: The function to call on this if it is an error value of a
matching type. By default this is identity.

> Example
Catching an `Illegal_Argument_Error` and returning its message.

from Standard.Base import all

example_catch =
error = Error.throw (Illegal_Argument_Error "My message")
error.catch Illegal_Argument_Error (err -> err.message)

> Example
Catching an erroneous value and getting the length of its message.
Catching any dataflow error and turning it into a regular value.

from Standard.Base import all

example_catch =
error = Error.throw "My message"
error.catch (err -> err.length)
catch : (Error -> Any) -> Any
catch (handler = x->x) = self.catch_primitive handler
error = Error.throw 42
error.catch == 42
catch : Any -> (Error -> Any) -> Any
catch (error_type = Any) (handler = x->x) =
self.catch_primitive error_value->
case error_value.is_a error_type of
True -> handler error_value
False -> self

## Transforms an error.

Expand Down
4 changes: 2 additions & 2 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Json.enso
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ from_pairs contents =
example_parse = Json.parse '{ "a": 1 }'
parse : Text -> Json ! Parse_Error
parse json_text =
Panic.catch Polyglot_Error (Internal.parse_helper json_text) caught_panic->
Error.throw (Parse_Error caught_panic.payload.cause.getMessage)
Panic.catch_java Any (Internal.parse_helper json_text) java_exception->
Error.throw (Parse_Error java_exception.getMessage)

## Represents a JSON structure.
type Json
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ into_helper fmt json = case fmt of
fnames = cons.fields
ffmts = m.fields
field_values = fnames.zip ffmts n-> inner_fmt->
fjson = json_fields . get n . catch _->
fjson = json_fields . get n . catch Any _->
Panic.throw (Missing_Field_Error json fmt n)
into_helper inner_fmt fjson
cons.new field_values
Expand Down
2 changes: 1 addition & 1 deletion distribution/lib/Standard/Base/0.0.0-dev/src/Data/Map.enso
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ type Map
example_get_or_else = Examples.map.get_or_else 2 "zero"
get_or_else : Any -> Any -> Any
get_or_else key ~other =
self.get key . catch (_ -> other)
self.get key . catch No_Value_For_Key_Error (_ -> other)

## Transforms the map's keys and values to create a new map.

Expand Down
8 changes: 3 additions & 5 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time.enso
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ now = Time ZonedDateTime.now
example_new = Time.new 1986 8 5
new : Integer -> Integer -> Integer -> Integer -> Integer -> Integer -> Integer -> Zone -> Time ! Time_Error
new year (month = 1) (day = 1) (hour = 0) (minute = 0) (second = 0) (nanosecond = 0) (zone = Zone.system) =
Panic.recover Any (Time (ZonedDateTime.of year month day hour minute second nanosecond zone.internal_zone_id)) . catch e-> case e of
Polyglot_Error err -> Error.throw (Time_Error err.getMessage)
x -> x
Panic.catch_java Any (Time (ZonedDateTime.of year month day hour minute second nanosecond zone.internal_zone_id)) java_exception->
Error.throw (Time_Error java_exception.getMessage)

## ALIAS Time from Text

Expand Down Expand Up @@ -122,8 +121,7 @@ new year (month = 1) (day = 1) (hour = 0) (minute = 0) (second = 0) (nanosecond

import Standard.Base.Data.Time

example_parse = Time.parse "2020-10-01" . catch e-> case e of
Time.Error _ -> Time.now
example_parse = Time.parse "2020-10-01" . catch Time_Error (_->Time.now)

> Example
Parse "2020-05-06 04:30:20" as Time
Expand Down
16 changes: 11 additions & 5 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date.enso
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,14 @@ today = now

new : Integer -> Integer -> Integer -> Date ! Time.Time_Error
new year (month = 1) (day = 1) =
Panic.recover Any (LocalDate.of year month day) . catch e-> case e of
## TODO This is left using the old error handling approach, because
magically, changing this to the `catch_java` (which is now preferred way
of catching Polyglot_Errors) lead to the "should format local date using
provided pattern" test failing because it called the `LocalDate.format`
instead of Enso format. Hopefully this will be fixed with
https://github.com/enso-org/enso/pull/3559
Then this should be switched to use `Panic.catch_java`.
Panic.recover Any (LocalDate.of year month day) . catch Any e-> case e of
Polyglot_Error err -> Error.throw (Time.Time_Error err.getMessage)
x -> x

Expand Down Expand Up @@ -107,8 +114,8 @@ new year (month = 1) (day = 1) =
import Standard.Base.Data.Time.Date
import Standard.Base.Data.Time

example_parse_err = Date.parse "my birthday" . catch e-> case e of
Time.Time_Error _ -> Date.new 2000 1 1
example_parse_err = Date.parse "my birthday" . catch Time.Time_Error _->
Date.new 2000 1 1

> Example
Parse "1999-1-1" as Date using a custom format.
Expand All @@ -125,8 +132,7 @@ new year (month = 1) (day = 1) =

example_parse_err =
date = Date.parse "1999-1-1" "yyyy-MM-dd"
date.catch e-> case e of
Time.Time_Error _ -> Date.new 2000 1 1
date.catch Time.Time_Error (_->Date.new 2000 1 1)
parse : Text -> (Text | Nothing) -> Date ! Time.Time_Error
parse text pattern=Nothing =
result = Panic.recover Any <| case pattern of
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ now = Time_Of_Day LocalTime.now
example_epoch = Time_Of_Day.new hour=9 minute=30
new : Integer -> Integer -> Integer -> Integer -> Time_Of_Day ! Time.Time_Error
new (hour = 0) (minute = 0) (second = 0) (nanosecond = 0) =
Panic.recover Any (Time_Of_Day (LocalTime.of hour minute second nanosecond)) . catch e-> case e of
Polyglot_Error err -> Error.throw (Time.Time_Error err.getMessage)
x -> x
Panic.catch_java Any (Time_Of_Day (LocalTime.of hour minute second nanosecond)) java_exception->
Error.throw (Time.Time_Error java_exception.getMessage)

## Obtains an instance of `Time_Of_Day` from a text such as "10:15".

Expand Down Expand Up @@ -97,8 +96,8 @@ new (hour = 0) (minute = 0) (second = 0) (nanosecond = 0) =
import Standard.Base.Data.Time
import Standard.Base.Data.Time.Time_Of_Day

example_parse = Time_Of_Day.parse "half twelve" . catch e-> case e of
Time.Time_Error _ -> Time_Of_Day.new
example_parse = Time_Of_Day.parse "half twelve" . catch Time.Time_Error _->
Time_Of_Day.new

> Example
Parse "04:30:20" as Time_Of_Day.
Expand Down
76 changes: 61 additions & 15 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Error/Common.enso
Original file line number Diff line number Diff line change
Expand Up @@ -61,24 +61,39 @@ type Error

Returns a human-readable text representing this error.
to_display_text : Text
to_display_text = "Error: " + (self.catch .to_display_text)
to_display_text = "Error: " + (self.catch Any .to_display_text)

## Executes the provided handler on a dataflow error, or returns a non-error
value unchanged.
## Executes the provided handler on an error, or returns the value unchanged.

Arguments:
- handler: The function to call on this if it is an error value. By default
this is identity.
- error_type: The type of error to handle. Defaults to `Any` to handle
all errors.
- handler: The function to call on this if it is an error value of a
matching type. By default this is identity.

> Example
Catching an erroneous value and getting the length of its message.
Catching an `Illegal_Argument_Error` and returning its message.

import Standard.Examples
from Standard.Base import all

example_catch =
Examples.throw_error.catch (err -> err.message.length)
catch : (Error -> Any) -> Any
catch (handler = x->x) = self.catch_primitive handler
error = Error.throw (Illegal_Argument_Error "My message")
error.catch Illegal_Argument_Error (err -> err.message)

> Example
Catching any dataflow error and turning it into a regular value.

from Standard.Base import all

example_catch =
error = Error.throw 42
error.catch == 42
catch : Any -> (Error -> Any) -> Any
catch (error_type = Any) (handler = x->x) =
self.catch_primitive error_value->
case error_value.is_a error_type of
True -> handler error_value
False -> self

## UNSTABLE

Expand All @@ -91,7 +106,7 @@ type Error

example_display = Examples.throw_error.to_default_visualization_data
to_default_visualization_data : Text
to_default_visualization_data = self.catch .to_default_visualization_data
to_default_visualization_data = self.catch Any .to_default_visualization_data

## UNSTABLE

Expand All @@ -106,8 +121,9 @@ type Error
to_json : Json.Object
to_json =
error_type = ["type", "Error"]
error_content = ["content", self.catch .to_json]
error_message = ["message", self.catch .to_display_text]
caught = self.catch
error_content = ["content", caught.to_json]
error_message = ["message", caught.to_display_text]
Json.from_pairs [error_type, error_content, error_message]

## Transforms an error.
Expand All @@ -127,7 +143,7 @@ type Error
map = Examples.map
map.get 10 . map_error (_ -> "The element 10 was not found.")
map_error : (Error -> Error) -> Any
map_error f = self.catch (x -> Error.throw (f x))
map_error f = self.catch Any (x -> Error.throw (f x))

## ADVANCED
UNSTABLE
Expand Down Expand Up @@ -297,7 +313,7 @@ type Panic

example_rethrow = Panic.rethrow Examples.throw_error
rethrow : (Any ! Any) -> Any
rethrow value = value.catch Panic.throw
rethrow value = value.catch Any Panic.throw

## Executes the provided action and if a panic matching the provided type was
thrown, calls the provided callback.
Expand Down Expand Up @@ -351,6 +367,36 @@ type Panic
False -> Panic.throw caught_panic
_ -> Panic.throw caught_panic

## Executes the provided action and if a Java exception matching the provided type was
thrown, calls the provided callback.

Normally, Java exceptions are wrapped in a `Polyglot_Error` instance, so
using a `Panic.catch` requires unwrapping the error by calling
`caught_panic.payload.cause`. This helper function allows the handler to
work with the Java exception directly. The downside is that if the Java
exception is rethrown, it will be rethrown as a Java exception object
wrapped in an Enso panic. So if the handler needs to rethrow the original
exception preserving its shape and stacktrace, `Panic.catch` should still
be preferred.`

> Example
Convert a string to an integer, catching the Java `NumberFormatException`
and converting it to a more Enso-friendly dataflow error.

polyglot java import java.lang.Long
polyglot java import java.lang.NumberFormatException
parse str =
Panic.catch_java NumberFormatException (Long.parseLong str) java_exception->
Error.throw (Illegal_Argument_Error "The provided string is not a valid number: "+java_exception.getMessage)
catch_java : Any -> Any -> (Throwable -> Any) -> Any
catch_java panic_type ~action handler =
Panic.catch_primitive action caught_panic-> case caught_panic.payload of
Polyglot_Error java_exception ->
case (panic_type == Any) || (Java.is_instance java_exception panic_type) of
True -> handler java_exception
False -> Panic.throw caught_panic
_ -> Panic.throw caught_panic

## Executes the provided action and converts a possible panic matching any of
the provided types into a dataflow Error.

Expand Down
2 changes: 1 addition & 1 deletion distribution/lib/Standard/Base/0.0.0-dev/src/Meta.enso
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ Base.Error.is_a typ = self.is_an typ
Arguments:
- typ: The type to check `self` against.
Base.Error.is_an : Any -> Boolean
Base.Error.is_an typ = typ == Base.Error
Base.Error.is_an typ = typ==Any || typ==Base.Error

## UNSTABLE
ADVANCED
Expand Down
12 changes: 4 additions & 8 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Network/Http.enso
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import Standard.Base.Network.Http.Request
import Standard.Base.Network.Http.Request.Body as Request_Body
import Standard.Base.Network.Http.Response
import Standard.Base.Network.Http.Version
import Standard.Base.Network.Internal
import Standard.Base.Network.Proxy
import Standard.Base.Network.Uri
import Standard.Base.System.File
Expand Down Expand Up @@ -597,11 +596,13 @@ type Http
http.request req
request : Request -> Response ! Request_Error
request req =
response = Panic.recover Any <|
handle_request_error =
Panic.catch_java Any handler=(err-> Error.throw (Request_Error err.getClass.getSimpleName err.getMessage))
Panic.recover Any <| handle_request_error <|
body_publishers = HttpRequest.BodyPublishers
builder = HttpRequest.newBuilder
# set uri
builder.uri (Internal.panic_on_error req.uri.internal_uri)
builder.uri (Panic.rethrow req.uri.internal_uri)
# prepare headers and body
req_with_body = case req.body of
Request_Body.Empty ->
Expand Down Expand Up @@ -651,11 +652,6 @@ type Http
http_request = builder.build
body_handler = HttpResponse.BodyHandlers . ofByteArray
Response.Response (self.internal_http_client.send http_request body_handler)
response.catch e-> case e of
Polyglot_Error err ->
Error.throw (Request_Error err.getClass.getSimpleName err.getMessage)
_ ->
Error.throw e

## PRIVATE

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Standard.Base.Network.Http.Form
import Standard.Base.Network.Http.Header
import Standard.Base.Network.Http.Method
import Standard.Base.Network.Http.Request.Body as Request_Body
import Standard.Base.Network.Internal
import Standard.Base.Network.Uri
import Standard.Base.System.File

Expand All @@ -26,7 +25,7 @@ import Standard.Base.System.File
example_new = Request.new Method.Post (Uri.parse "http://example.com")
new : Method -> (Text | Uri) -> Vector.Vector -> Request_Body -> Request
new method addr (headers = []) (body = Request_Body.Empty) =
Panic.recover Any (Request method (Internal.panic_on_error (addr.to_uri)) headers body) . catch Internal.recover_panic
Panic.recover Any (Request method (Panic.rethrow (addr.to_uri)) headers body)

## Create an Options request.

Expand Down
17 changes: 0 additions & 17 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Network/Internal.enso

This file was deleted.

Loading

0 comments on commit 28513a3

Please sign in to comment.