Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  • ParsePort checks one number and fails with the reason.
  • ParseEndpoint can fail on its own, with no :, and calls ParsePort. When that fails it returns at once, building its own failure from port.Failure( ). A Result( of int32, ... ) and a Result( of Endpoint, ... ) are different types, so the failure is re-wrapped rather than returned as it is.
  • LoadSetting adds the setting's name to any failure from below. On success ep already 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

  1. Make ParseEndpoint refuse an empty host, and check that LoadSetting adds its context to the new failure without any change of its own.
  2. Change the loop to stop at the first failing setting, as a configuration loader that must not run with a half-read file would.