Error handling
All expressions with the exception of collections can evaluate as an error value. For instance when reading a dataset as a string, from a wrong URL:
let x = String.Read(Http.Get("http://broken/location"))
Because the URL http://broken/location
can't be downloaded, String.Read
evaluates as an error:
"service error: http error: host not found for http://broken/location"
An error is a value. It doesn't interrupt the execution when it occurs. In the example below,
x
is declared as a list of two items, two strings read from two URLs using
String.Read
.
The first call to String.Read
fails because the host doesn't
resolve, and evaluates as an error. The second call succeeds. Both values end
up in the list.
let x = [
String.Read(Http.Get("http://broken/location")), // that one fails
String.Read(Http.Get("http://www.example.com")) // that one succeeds
]
Here's the value of x
:
[
"http error: host not found for http://broken/location",
"... <the HTML page downloaded from www.example.com> ..."
]
Error propagation
If an error value is passed as a parameter to a function, the error propagates.
In the program below, List.Transform
is used to apply String.Length
to both
items of x
. When it is applied to the error value, String.Length
cannot compute
the string length and evaluates as an error:
let x = [
String.Read(Http.Get("http://broken/location")),
String.Read(Http.Get("http://www.example.com"))
]
in List.Transform(x, s -> String.Length(s))
The code executes till the end and returns a list of items, one of which is an error.
[
"http error: host not found for http://broken/location",
1256
]
Checking for errors
To check if a value is an error, use Try.IsError
.
let
x: string = String.Read(Http.Get("http://broken/location"))
in
Try.IsError(x) // true
Try.IsSuccess
is also available.
let
x: string = String.Read(Http.Get("http://broken/location"))
in
Try.IsSuccess(x) // false
Building and returning custom errors
Building a custom error value is also possible with Error.Build
. This is
useful if willing to return an error from a function. Here is an example
function that divides by two its argument n
, but returns an error when the
number isn't even:
let
half(n: int) =
if n % 2 == 0 then
n / 2
else
Error.Build(String.From(n) + " isn\'t even")
in
List.Transform([1, 2, 3, 4], (i) -> half(i))
evaluates to:
[
"1 isn't even",
1,
"3 isn't even",
2
]
Example
A call can fail unexpectedly: for instance, reading data from an external source can fail because the source cannot be reached or because it contains wrong data.
If you want to try this example, you can deploy the following endpoint:
Tutorial 11 - Handling errors
- Overview
- Code
Usage:
/tutorial/11-error-handling
main() =
let
urls = [
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-01.json",
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-02.json",
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-03.json",
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/not_found.json", // doesn't exist
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/airports.csv" // fails to parse as JSON
]
in
List.Transform(
urls,
(url) ->
let
contents = Json.Read(
url,
type record(creation_date: string, entries: list(record(hostname: string)))
),
nEntries = List.Count(contents.entries),
fixedCount = if Try.IsError(nEntries) then
-1
else
nEntries
in
{location: url, count: fixedCount}
)
As an example, suppose we are reading data from a list of remote locations (HTTPS endpoints).
We open each location with Json.Read
and then run List.Count
.
let urls = [
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-01.json",
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-02.json",
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-03.json",
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/not_found.json", // doesn't exist
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/airports.csv" // fails to parse as JSON
]
in List.Transform(urls, url ->
let contents = Json.Read(url, type record(creation_date: string, entries: list(record(hostname: string)))),
nEntries = List.Count(contents.entries)
in {location: url, count: nEntries })
If we execute the query above, the two last rows show an error in the count
field instead of the expected value.
That's because the URLs do not exist or are not JSON data.
[
{
"location": "https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-01.json",
"count": 2
},
{
"location": "https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-02.json",
"count": 2
},
{
"location": "https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-03.json",
"count": 3
},
{
"location": "https://raw-tutorial.s3.eu-west-1.amazonaws.com/not_found.json",
"count": "http error: could not read (HTTP GET) from https://raw-tutorial.s3.eu-west-1.amazonaws.com/not_found.json: (404)\n<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message><Key>not_found.json</Key><RequestId>0QY68Y95EK2FYQRR</RequestId><HostId>XoGyiNc8l9ZF33fv5/DsyDiBiN50nDa2DOaOovb+7ExYF4N3ddrBFxfVfGJGXufQRQp1NW3ArzY=</HostId></Error>"
},
{
"location": "https://raw-tutorial.s3.eu-west-1.amazonaws.com/airports.csv",
"count": "expected { but token VALUE_STRING found"
}
]
Let's now replace the errors with different data.
For this we use Try.IsError
with an if
statement.
let ...
nEntries = List.Count(contents.entries), // can be an error
fixedCount = if Try.IsError(nEntries) then -1 else nEntries
The code is now:
let urls = [
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-01.json",
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-02.json",
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-03.json",
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/not_found.json", // doesn't exist
"https://raw-tutorial.s3.eu-west-1.amazonaws.com/airports.csv" // fails to parse as JSON
]
in List.Transform(urls, url ->
let contents = Json.Read(url, type record(creation_date: string, entries: list(record(hostname: string)))),
nEntries = List.Count(contents.entries),
fixedCount = if Try.IsError(nEntries) then -1 else nEntries
in {location: url, count: fixedCount })
And the results are:
[
{
"location": "https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-01.json",
"count": 2
},
{
"location": "https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-02.json",
"count": 2
},
{
"location": "https://raw-tutorial.s3.eu-west-1.amazonaws.com/error-handling/log-2022-04-03.json",
"count": 3
},
{
"location": "https://raw-tutorial.s3.eu-west-1.amazonaws.com/not_found.json",
"count": -1
},
{
"location": "https://raw-tutorial.s3.eu-west-1.amazonaws.com/airports.csv",
"count": -1
}
]