#Soroban Contract Errors

48 messages · Page 1 of 1 (latest)

terse island
#

Hello all! Was having a discussion with @lofty storm regarding the usage of Errors in the Blend Protocol and wanted to move the convo to a public location.

Context:
]leigh: I'd like to understand how you're currently using errors.
[2:49 PM]leigh: As well as hear more about the issues you've come across, ambiguity about how to use them, which options to pick, etc.
[2:49 PM]leigh: No feedback is too small in this area.
[3:10 PM]mootz12: The feedback I mostly had was it wasn't clear to me how errors were intended to be used while writing contracts, and by extension consuming them.

Contracts currently have the ability to return a Result<thing, Error> or thing. Either way, the Client that gets generated on consumption is the same. It makes it unclear if a contract supports returning Errors or not. By habit I return Result<thing, error> if I ever expect my contract call to be handled on failure, and thing if I don't (which, to be fair, is only in the case of view functions).

I think the expectation gets further obfuscated with the pattern of panic_with_error macro and the auto try_ functions. (This could be thrown from anywhere in the codepath, and isn't really identifiable from the contract entry point)

I don't think the Blend protocol handles errors in a consistent or smart way TBH. The initial idea was consistent, unique codes for error groups (like http status codes). I'd have some hope the community could adopt standards for error codes (like, 403 Forbidden if the TX is signed validly but from an address that doesn't have enough privilege). The primary goal here would be to hopefully decode the problem of getting Result(ContractError(3)) and having no idea what that means, or what contract threw this error.

lofty storm
#

Sounds like the lack of rigid symmetry with how errors are defined, and then how they are consumed, makes it pretty unclear when errors should be returned and when they should be handled. It's natural to expect if a function returns a Result<_,_> that anyone consuming the function will see that return value always, but that's not what happens on Soroban.

#

Soroban errors returned from contract functions are more flexible than a regular function call, but that flexibility makes it really ambiguous for what should be done when.

#

Is that accurate?

terse island
#

Yup, I would agree!

#

Ensuring that it's clear for contract consumers (specifically other contracts) when a dependency is intending to return error codes seems useful

waxen wing
#

just saw this chat and thought i'd ask a question i probably already know the answer to, but is any method by which you can dynamically define the error? IIRC they have to be defined statically as an enum

lofty storm
#

Between contracts errors are communicated as a Status::ContractError(u32).

#

Thinking out loud for how we could address the symmetry:

#

It's somewhat tricky. Contract functions are in some ways inherently asymmetrical, because:

• All contract functions can fail regardless of whether they return a Result<_, E> or not.

• In most cases we expect folks to want a failing contract function to cause the entire call tree to fail immediately.

The first point is fixed and can't be changed.

The second point we could change by making every client call return a Result<_, E>, but it'd be inconvenient to a majority who don't care about the failure cases. But that's an assumption. @terse island Is our assumption incorrect?

#

The client provides two ways to call contracts.

• The bare function (e.g. .transfer(...)) which will panic on failure. i.e. If transfer fails, the calling function will also fail.

• The try function (e.g. .try_transfer(...)) which will always return a result (excluding fee exhaustion).

The only reason to use try_ is if the caller doesn't want to abort on failure.

We expect the common case is callers abort on failure. Do folks here think that is correct?

#

Thinking about how we could communicate how errors work better, there's two things developers should know, and I'm not sure we're communicating these things clearly enough:

• All contract functions can fail, with defined errors and undefined errors.

• Callers decide if they want to handle errors or abort.

terse island
#

I'd assume the large majority of cases are that most consuming contracts will not care about the error that is failing, and would opt to have the error immediately fail the transaction, regardless of what it is. Solidity supports handling errors on the consumer end, and I've rarely seen it used.

If all client functions return a Result, seems like that would be easy enough to manage with unwrap or ?.

That being said, I agree you can't force how contracts can fail, and I think its a good idea to give callers the ability to handle or abort. Maybe my gripe with the current situation is it feels confusing for callers to handle errors if they want, since it's difficult to determine if something was developed with the intention of having errors handled.

#

Something as simple as being proactive about including Error code numbers in SEPs for Soroban interfaces seems like a good thing to do regardless, but would significantly help devs who want to handle errors.

lofty storm
#

it feels confusing for callers to handle errors if they want, since it's difficult to determine if something was developed with the intention of having errors handled
We could modify the client and surface the Result<_, E> on the bare function if the contract function returns Result<_, E>. This might better communicate the intent of the contract author. 🤔

terse island
#

Can the wrapped try_{FN_NAME} explicitly fail for any reason, or will a result always be returned? IE - what if FN_NAME threw with panic!("this is bad!")

lofty storm
#

try_ catches any error/panic/failure, except fee exhaustion.

#

Fee exhaustion can't be caught because money has run out, so no more instructions can be executed. But any other error/panic/failure is caught by try.

waxen wing
#

I tried doing that with a string and i couldn't get it to work btw alex

lofty storm
#

The panic strings do not get passed to the caller.

waxen wing
#

it would be nice if there was an easy way to pass those through

lofty storm
#

The only way to pass a value in the error case is by returning the error, or calling panic_with_error!.

waxen wing
#

but i think i can make the int32 work just as well

terse island
#

Hm, yeah I wouldn't expect a client.FN_NAME -> Result<i128, Error> to catch on a panic!, only if it returns an Error, to remain consistent with normal Rust error usage.

Could the contract interface be created as is (things with Result have result, and things without don't, where it behaves like normal Rust panics don't propagate?), and the client gives you the option to client.try().transfer that wraps whatever the contract defines? The intention here being just to expose whatever the contract writer wrote, but also allow for non-intended failures to be caught if desired

waxen wing
#

yeah no i tried panic_with_error!("string") and that just gives a host error

lofty storm
terse island
#

it has the downside of some things being Result<Result<i128, MyContractError>, SorobanError>

waxen wing
#

but it will allow for more dynamic handling than just a few enum options

#

and it will keep the wasm size down by not defining 50 errors in the enum

#

(completely unrelated but) You guys should respond to that thread on allocators me and matias are both wondering about potential use cases for alloc.

lofty storm
#

If we're going to expose Result<> on the bare functions, it seems like it might be less surprising to developers, and create more consistency, if we made the bare functions the try functions and just always returned Result<>.

#

For contract functions that don't actually return an error, the client would be Result<_, Status>. For contract functions that do return an error, the client would be Result<_, E>.

waxen wing
#

seems there will always be some then. What is the shape of Status?

#

oh nevermind the Result<> isn't returned directly from the contract right it's generated by the host?

coral plaza
cunning walrus
#

Just thinking outloud and high-level, but perhaps some automatic code generation could be done to wrap fns to a try_fn ?

fn my_call(..) {} // can panic!

// and decorating the above to wrap error? 
#[Error(ErrCode)]
fn my_call(...) {} // create try__my_call wrapper that returns Result<_,ErrCode> 

That way devs can just focus on their fns and simply wrap those they think should return Err?

#

(I know decorators are a thing some don't people like, but they def. help and are more understandable for understanding code, IMO)

terse island
terse island
coral plaza
#

One thing I want to weave in: try / catch / error handling routines didn't pick up in solidity because they increase NPath Complexity (number of execution path possible, as most probably know) quite heavily & that in turn increases computation time and therefore costs.

ink! has a mixed approach: https://use.ink/ink-vs-solidity#errors-and-returning

In general, Result::Err should be used when a calling contract needs to know why a function failed. Otherwise, assert! should be used as it has less overhead than a Result.

I think that is a good way to go.

cunning walrus
#

I guess cheap wins over correct in crypto 😆

coral plaza
#

My prof always said "doing it right is never overhead", but that was JEE consultant style.