Propagating errors
A function that calls another fallible function usually wants to stop at the
first failure and hand it to its own caller, adding what it knows on the way.
With a Result that is an early return.
#import "std/outcome"
type Endpoint
as ustring host
as int32 port
end type
type PortResult as outcome.Result( of int32, ustring )
type EndpointResult as outcome.Result( of Endpoint, ustring )
'' Level 3: one number.
function ParsePort( byref text as ustring ) as PortResult
var port = valint( text )
if str( port ) <> text orelse port < 1 orelse port > 65535 then
var why = "bad port '" & text & "'"
return outcome.Fail( of int32, ustring )( why )
end if
return outcome.Ok( of int32, ustring )( port )
end function
'' Level 2: "host:port". A failure below is passed up unchanged.
function ParseEndpoint( byref text as ustring ) as EndpointResult
var colon = instr( text, ":" )
if colon = 0 then
var why = "no ':' in '" & text & "'"
return outcome.Fail( of Endpoint, ustring )( why )
end if
var port = ParsePort( mid( text, colon + 1 ) )
if port.IsOk( ) = false then
return outcome.Fail( of Endpoint, ustring )( port.Failure( ) )
end if
dim ep as Endpoint
ep.host = left( text, colon - 1 )
ep.port = port.Value( )
return outcome.Ok( of Endpoint, ustring )( ep )
end function
'' Level 1: a named setting. A failure gains context on the way up.
function LoadSetting( byref key as ustring, byref text as ustring ) _
as EndpointResult
var ep = ParseEndpoint( text )
if ep.IsOk( ) = false then
var why = key & ": " & ep.Failure( )
return outcome.Fail( of Endpoint, ustring )( why )
end if
return ep
end function
dim keys( 0 to 2 ) as ustring = { "db", "cache", "mail" }
dim texts( 0 to 2 ) as ustring = { "db.local:5432", "cache.local", _
"mail.local:25x" }
for i as int32 = 0 to 2
var r = LoadSetting( keys( i ), texts( i ) )
if r.IsOk( ) then
print keys( i ); " -> "; r.Value( ).host; " port"; r.Value( ).port
else
print "error: "; r.Failure( )
end if
next
Output
db -> db.local port 5432
error: cache: no ':' in 'cache.local'
error: mail: bad port '25x'
Three levels of calls, each either handling a failure or passing it on:
ParsePortchecks one number and fails with the reason.ParseEndpointcan fail on its own, with no:, and callsParsePort. When that fails it returns at once, building its own failure fromport.Failure( ). AResult( of int32, ... )and aResult( of Endpoint, ... )are different types, so the failure is re-wrapped rather than returned as it is.LoadSettingadds the setting's name to any failure from below. On successepalready has the right type and is returned unchanged.
Nothing below the top level prints anything. The loop at the bottom is the one place that decides what a failure means to the user, and it sees exactly which setting failed and why.
The cost is one branch per call, paid where the decision is made. Compare a
runtime error from error n, which jumps to a handler and, on the way out of
a procedure, skips its cleanup; see Error numbers and
handlers.
Activities
- Make
ParseEndpointrefuse an empty host, and check thatLoadSettingadds its context to the new failure without any change of its own. - Change the loop to stop at the first failing setting, as a configuration loader that must not run with a half-read file would.