icerpc/slicec

Replace throws by result, exception by error?

Closed this issue · 40 comments

The Slice throws syntax suggests an exception but it's really like a Result, as described in the Getting started:

https://docs.testing.zeroc.com/getting-started/raising-the-bar/slice-better-idl#better-idl-for-rpcs

https://github.com/icerpc/icerpc-docs/blob/c89680d0e7b2299d9824c494330e77ed4cb61c3b/pages/getting-started/raising-the-bar/slice-better-idl.md?plain=1#L71

We could consider actually using result and error in Slice, i.e. rewrite this example as:

interface Translator {
    translate(message: string) -> result<string, TranslationError>
}

error TranslationError {
    pos: int32
    detectedLanguage: string?
}

where result and error are new keywords.

pepone commented

What will be the syntax for a void operation with a custom error?

We could add support for a unit type, written as an empty tuple: ()

start_database(options: DatabaseOptions) -> result<(), DatabaseError>

The compiler is already capable of handling this type, we just artificially disallow it because it's useless right now:

if return_tuple.len() < 2 {
    let diagnostic = Diagnostic::new(Error::ReturnTuplesMustContainAtLeastTwoElements).set_span(&span);
    parser.diagnostics.push(diagnostic);
}

A user could also just return an optional error:

start_database(options: DatabaseOptions) -> DatabaseError?

I don't find this semantically compelling, but it would work right now on main and gets the job done without adding ().

A user could also just return an optional error:

This would be ambiguous. Error/Exception are regular encodable types in Slice2. What if you want to return an error/exception as your return type?

For interop with Slice1, I guess the mechanistic translation would be:
AnyException <---> Error
throws AnyException <---> result<..., Error>
throws MyException <---> result<..., MyException>

bentoi commented

Can't we simply rename the exception keyword with a new error keyword?

Replacing exception with an error concept would be better. When reading the documentation, it really feels like: "we named this exception but it's a bad name because ...".

How would the TranslateError name be mapped in C#? TranslateErrorException?

Restated proposal

Replace exception by error class | struct

The error keyword replaces the exception keyword with identical semantics except errors have by convention the Error suffix while exceptions have by convention the Exception suffix.

Unlike exception, error is not a separate construct but a qualifier for class (Slice1) or struct (Slice2). With Slice1, an error class can only derive from another error class and cannot be used as a field/parameter/sequence element/dictionary value type. With Slice2, an error struct cannot be compact.

I anticipate we'll add error enum in the future to Slice2, when we support enum with associated values.

The advantage of "downgrading" error to a qualifier is it's one less constructed type to document and implement. The encoding is already the same.

For Slice1, the error/exception name and the different suffix matters since the error/exception name generates a type ID used for encoding/decoding. Slice2 (fortunately) doesn't use type IDs for encoding/decoding.

As a consequence, for interop with Ice, you want to keep using the Exception suffix for your Slice1 error class names.

Then, when mapping an error to C# or any other language where the mapping uses exceptions for Slice errors, the Error suffix is replaced by an appropriate language-mapping dependent suffix. For example:
error struct FooError => C# FooException

We also rename AnyException => AnyError.

See also: icerpc/icerpc-csharp#3571

A consequence of this proposal is that an error class is always Slice1-only, whereas today a Slice1 exception without a base exception can be used as a Slice2 exception, at the cost of more complicated generated code. We'd get rid of this code & associated functionality.

Replace throws by result type

-> result<R, E> is the new syntax for -> R throws E

Where E must be an error class | struct depending on the compilation mode.

You can also use:

op() -> R // no exception/error
op() -> result<(), E> // returns "void" or an error

However, the following are/remain syntax errors:

op() -> () // not allowed
op() -> result<()> // not allowed
op() -> result<string> // not allowed
op() -> result<string, ()> // not allowed 

Replace @throws by @error in doc comments

Can't we simply rename the exception keyword with a new error keyword?

It's more than that, see above.

Replacing exception with an error concept would be better. When reading the documentation, it really feels like: "we named this exception but it's a bad name because ...".

Yes, it's a bad name/syntax. I wish we could fix this for 0.1.

How would the TranslateError name be mapped in C#? TranslateErrorException?

Slice error struct TranslateError would map to C# class TranslateException.

bentoi commented

I don't like much exception or result<R, E>, it's very much like programming Java/C# exceptions or the Rust error model.

Could a middle ground be to use the error keyword for operations that return an error and only support this keyword with Slice2? For Slice1, we keep exception.

Ideally with Slice2, it would be great if we didn't have to introduce something like error struct or error enum.

// Slice2
compact struct GreeterError
{
    message: string
}
greet(name: string) -> string error GreeterError

// Slice1
exception GreeterException
{
    message: string
}
greet(name: string) -> string throws GreeterException

In C#, with Slice1 GreeterException maps to GreeterException.

With Slice2, I'd still use an exception since that's how C# recommends reporting errors. Could the above generic both GreeterError and GreeterException where GreeterException is generated when parsing the first Slice operation that returns error GreeterError? The exception would have a GreeterError Error property.

Another possibility could be to not generate an exception but use a generic SliceException<T> type where T is GenericError. It's not very common, I only found one such generic exception type here: https://learn.microsoft.com/en-us/dotnet/api/system.servicemodel.web.webfaultexception-1?view=netframework-4.8.1

Result is the standard approach. It's not Rust-only. That's also what Swift uses:
https://developer.apple.com/documentation/swift/result

See also:
https://en.wikipedia.org/wiki/Result_type

Even in language without a built-in Result, like C#, libraries that provide this feature call it Result. So we should definitely use Result!

bentoi commented

It's not because some languages support a result type that we should provide one in mappings that don't.

There's no such type in the .NET base class library and Microsoft best practice is to throw an exception instead of returning an error: https://learn.microsoft.com/en-us/dotnet/standard/exceptions/best-practices-for-exceptions#throw-exceptions-instead-of-returning-an-error-code.

Please google "result type in c#". Also, let's not mix-up this Slice language proposal (here) and the separate Slice-to-C# mapping proposal (icerpc/icerpc-csharp#3571).

I'm totally fine with adopting the Result model over the throws model.
Maybe I'm just Rust-biased, but I do think it would be easier to map to more languages, and better represents RPC semantics.

But I have some reservations about the exception vs error part. Just the proposed semantics, not the name.
I would 100% support renaming the exception keyword to error, like @benoit suggested.


The advantage of "downgrading" error to a qualifier is it's one less constructed type to document and implement. The encoding is already the same.

It's not clear this is an avantage when you look at the whole picture.
We're exchanging "1 constructed type in 1 place" for "1 keyword in 3 places" (class, struct, enum).
This would be less centralized and easier to miss as yet-another-modifier mixed in with others (compact ids, unchecked, compact).


Then, when mapping an error to C# or any other language where the mapping uses exceptions for Slice errors, the Error suffix is replaced by an appropriate language-mapping dependent suffix. For example:
error struct FooError => C# FooException

I'm generally not a fan of Slice modifying your identifiers when you use special words.
It feels like our old trick for massaging I prefixes to mapped interface names.
What's the problem with doing:

error struct Foo {} // Slice
class FooException: Exception {} // C#
struct FooError {} // Rust

I also think this would drive a larger wedge between Slice1 and Slice2 modes:
"You should always end your error types with 'Error', and if you do, Slice will specially map the identifier"
"But you definitely shouldn't do this in 'Slice1' mode because it will break OTW compatibility."


And speaking of wedges between the 2 modes, I think that's also made worse by:

  • you have to use (and can only use) error class in Slice1 mode.
  • you have to use (and can only use) error struct in Slice2 mode.

One great advantage of this is it's less magical.
Right now the behavior of an exception is VERY dependent on what mode it's defined in.
But with this new syntax, it's obvious how it will behave. error class acts like a class, etc.

But, it hurts code reuse, and breaks an otherwise okay upgrade path.
With a single type exception that is shareable between modes, it's possible to seamlessly transition.
With this proposal, your error types lock you into a mode with no clear path for upgrade.

Nailing down the specifics of the Result proposal:

Valid Syntaxes

These are all the ways you can specify the return type of an operation:

// Infallible Operations
myOp()
myOp() -> bool
myOp() -> (a: bool, b: bool)

// Fallible Operations
myOp() -> Result<(), E>
myOp() -> Result<bool, E>
myOp() -> Result<(a: bool, b: bool), E>

Where E must be an "error" type, and cannot be optional.
Slice provides no built-in "error" types, they must all be user-defined.

The "Ok" type can be any Slice type, including optionals.

Terminology

I think logically, everything that comes after the -> is the "return type".
So the "return type" of myOp() -> Result<(), E> is Result<(), E>.
Just like the "return type" of myOp() -> string is string.

So what do we call the generic parameters that go in the Result?
ie. for Result<A, E> what do we call A and E?

Success and failure types? Ok and Error types? etc.

#̵#̵#̵ ̵E̵r̵r̵o̵r̵ ̵S̵t̵r̵u̵c̵t̵
̵̵e̵r̵r̵o̵r̵ ̵s̵t̵r̵u̵c̵t̵̵s̵ ̵a̵r̵e̵ ̵i̵d̵e̵n̵t̵i̵c̵a̵l̵ ̵i̵n̵ ̵s̵y̵n̵t̵a̵x̵ ̵a̵n̵d̵ ̵f̵u̵n̵c̵t̵i̵o̵n̵ ̵t̵o̵ ̵n̵o̵r̵m̵a̵l̵ ̵̵s̵t̵r̵u̵c̵t̵s̵̵ ̵w̵i̵t̵h̵ ̵t̵h̵e̵ ̵f̵o̵l̵l̵o̵w̵i̵n̵g̵ ̵d̵i̵f̵f̵e̵r̵e̵n̵c̵e̵s̵:̵
̵-̵ ̵C̵a̵n̵n̵o̵t̵ ̵b̵e̵ ̵m̵a̵r̵k̵e̵d̵ ̵̵c̵o̵m̵p̵a̵c̵t̵̵.̵
̵-̵ ̵C̵a̵n̵ ̵o̵n̵l̵y̵ ̵b̵e̵ ̵d̵e̵c̵l̵a̵r̵e̵d̵ ̵i̵n̵ ̵̵S̵l̵i̵c̵e̵2̵̵ ̵m̵o̵d̵e̵.̵ ̵(̵G̵e̵t̵ ̵t̵h̵i̵s̵ ̵f̵o̵r̵ ̵f̵r̵e̵e̵ ̵b̵e̵c̵a̵u̵s̵e̵ ̵o̵n̵l̵y̵ ̵̵c̵o̵m̵p̵a̵c̵t̵̵ ̵s̵t̵r̵u̵c̵t̵s̵ ̵a̵r̵e̵ ̵v̵a̵l̵i̵d̵ ̵i̵n̵ ̵S̵l̵i̵c̵e̵1̵)̵
̵-̵ ̵C̵a̵n̵ ̵b̵e̵ ̵u̵s̵e̵d̵ ̵a̵s̵ ̵t̵h̵e̵ ̵e̵r̵r̵o̵r̵ ̵t̵y̵p̵e̵ ̵o̵f̵ ̵a̵ ̵̵R̵e̵s̵u̵l̵t̵̵ ̵(̵w̵h̵e̵r̵e̵a̵s̵ ̵n̵o̵r̵m̵a̵l̵ ̵s̵t̵r̵u̵c̵t̵s̵ ̵c̵a̵n̵n̵o̵t̵ ̵b̵e̵)̵.̵
̵-̵ ̵S̵h̵o̵u̵l̵d̵ ̵e̵n̵d̵ ̵w̵i̵t̵h̵ ̵a̵n̵ ̵̵E̵r̵r̵o̵r̵̵ ̵s̵u̵f̵f̵i̵x̵ ̵b̵y̵ ̵c̵o̵n̵v̵e̵n̵t̵i̵o̵n̵.̵
̵-̵ ̵M̵a̵p̵p̵e̵d̵ ̵t̵o̵ ̵c̵l̵a̵s̵s̵e̵s̵ ̵i̵n̵ ̵C̵#̵ ̵(̵i̵n̵s̵t̵e̵a̵d̵ ̵o̵f̵ ̵s̵t̵r̵u̵c̵t̵s̵)̵
̵
̵I̵s̵ ̵t̵h̵e̵r̵e̵ ̵a̵n̵y̵t̵h̵i̵n̵g̵ ̵e̵l̵s̵e̵?̵
̵F̵o̵r̵ ̵i̵n̵s̵t̵a̵n̵c̵e̵,̵ ̵i̵s̵ ̵i̵t̵ ̵s̵t̵i̵l̵l̵ ̵v̵a̵l̵i̵d̵ ̵t̵o̵ ̵h̵o̵l̵d̵ ̵a̵n̵ ̵̵e̵r̵r̵o̵r̵ ̵s̵t̵r̵u̵c̵t̵̵ ̵e̵v̵e̵r̵y̵w̵h̵e̵r̵e̵ ̵a̵ ̵̵s̵t̵r̵u̵c̵t̵̵ ̵w̵o̵u̵l̵d̵ ̵b̵e̵.̵ ̵L̵i̵k̵e̵:̵
̵̵̵̵ ̵o̵p̵(̵d̵a̵t̵a̵:̵ ̵N̵o̵t̵F̵o̵u̵n̵d̵E̵r̵r̵o̵r̵)̵ ̵/̵/̵ ̵a̵s̵ ̵a̵ ̵p̵a̵r̵a̵m̵e̵t̵e̵r̵ ̵t̵y̵p̵e̵?̵ ̵I̵t̵ ̵j̵u̵s̵t̵ ̵g̵e̵t̵s̵ ̵t̵r̵e̵a̵t̵e̵d̵ ̵l̵i̵k̵e̵ ̵a̵ ̵n̵o̵r̵m̵a̵l̵ ̵s̵t̵r̵u̵c̵t̵ ̵a̵t̵ ̵t̵h̵e̵ ̵S̵l̵i̵c̵e̵ ̵l̵e̵v̵e̵l̵.̵ ̵̵̵̵

Revised proposal

Incorporates Austin's and Benoit's feedback.

Keep exception for Slice1

We keep the exception syntax for Slice1. exception FooException remains the same as today. exception becomes Slice1 only just like class. We also keep AnyException as is.

Rationale:

  • Slice1 is primarily for compatibility with Ice where we can't change the exception type IDs (= Exception suffix)
  • the encoding of classes and (user) exceptions is actually slightly different in Slice1

Replace throws by result<Success, Failure> (or result<Ok, Error>)

We use the syntax presented in previous proposals. void = (). We use this syntax for both Slice1 and Slice2.

For Slice1, the Failure/Error must be an exception.

For Slice2, the Failure/Error can be any struct (including compact struct). In the future, we'll probably allow enums too.

No new error keyword

Any Slice2-capable struct can be used as an error in a Slice2 operation

C# mapping

Slice1: no change
exception FooException is mapped to class FooException derived from SliceException.

SliceException becomes a Slice1-only class, just like SliceClass is Slice1-only.

Slice2: when op() fails with custom error MyStruct, the invocation throws an IceRpc.Slice.DispatchException<MyStruct>, where DispatchException<TError> is:

public sealed class DispatchException<TError> : System.Exception
{
    public TError Error { get; }

    // When decoded & thrown from an IceRPC invocation, message comes from the response header
    public DispatchException(TError error, string? message = null, Exception? innerException = null) { ... } 
}

The client making the invocation can then catch this custom error as follows:

try
{
    proxy.OpAsync();
}
catch (DispatchException<MyStruct> exception)
{
    ... my custom error
}
catch (DispatchException exception)
{
    ... some other error from the dispatch
}

Other language mapping (Slice2)

For other language mappings, the actual Error/Exception returned/thrown won't be the raw mapped Slice struct but a holder that uses the IceRPC response error message to implement the Error/Exception trait, just like in C#.

Sounds good to me!

bentoi commented

Sounds good for Slice1, it's consistent with the class support.

For Slice2, I'm reserved by how we specify errors on Slice operations. Other than Rust, I didn't find any other language that use result<S, E>. I find using the return param misleading for all the exception based languages (C++, Java, C#, Python, etc). And for Swift, errors are not specificied in the result. In Go it's by convention the last return param. So I still have a preference for op() -> R error E.

For the C# mapping, it's not clear to me why we have DispatchException for Slice2 and SliceException for Slice1. Why not just SliceException and a generic SliceException<T> for Slice2?

However, I think it would be good to discuss generating MyException : SliceException for both Slice1 and Slice2. I find it is less surprising for exception based languages. We could generate a FooException class with a FooError Error property if an operation specifies a FooError error (such as op() -> error FooError). With Slice2, by convention, both error Foo and error FooError would generate FoeException in exception based languages.

bentoi commented

Any Slice2-capable struct can be used as an error in a Slice2 operation

For Swift, I suppose we would add type extension to the generated struct to add the Error protocol?

For Slice2, I'm reserved by how we specify errors on Slice operations. Other than Rust, I didn't find any other language that use result<S, E>

I don't understand this argument. Result<Success, Failure> is common place, it's not just Rust. See for example:
https://en.wikipedia.org/wiki/Result_type

There is a widely used standard pattern called Result and yet you propose to use a different syntax? Why?

In Swift, we would definitely return a Result and not throw an exception for dispatch errors.

Also, my proposal is to use -> result<S, E> for all compilation modes, not just Slice2. For Slice1, E would be an exception or AnyException.

For the C# mapping, it's not clear to me why we have DispatchException for Slice2 and SliceException for Slice1. Why not just SliceException and a generic SliceException for Slice2?

ZeroC.Slice.SliceException is the base class for Slice-mapped exception, is Slice1-only, and corresponds to the Slice1-only AnyException keyword. It's comparable to ZeroC.Slice.SliceClasss and AnyClass. Both are in the ZeroC.Slice assembly in C#.

IceRpc.Slice.DispatchException<TError> is an error holder. It's provided by the IceRPC + Slice integration together with its sibling, IceRpc.Slice.DispatchException.

However, I think it would be good to discuss generating MyException : SliceException for both Slice1 and Slice2. I find it is less surprising for exception based languages. We could generate a FooException class with a FooError Error property if an operation specifies a FooError error (such as op() -> error FooError). With Slice2, by convention, both error Foo and error FooError would generate FoeException in exception based languages.

You can certainly propose it, but I don't think we should do that. It would be both confusing and unworkable:

  • in which namespace would you generate this C# exception?
  • what happens when 2 operations in 2 different Slice files return the same error?

Any Slice2-capable struct can be used as an error in a Slice2 operation

For Swift, I suppose we would add type extension to the generated struct to add the Error protocol?

No, not at all. That's what I explained here:

For other language mappings, the actual Error/Exception returned/thrown won't be the raw mapped Slice struct but a holder that uses the IceRPC response error message to implement the Error/Exception trait, just like in C#.

The generated struct / enum (etc.) is not augmented in any way because it's returned as an error. Not by the Slice compiler, and typically not by the user (of course the users can always do whatever they want).

In all language mappings, the struct / enum (etc) returned as an error is ALWAYS "wrapped" by a holder struct/class that itself implements any required/conventional Error trait/protocol.

With my proposal, the C# holder is DispatchException<T>. And the trait/protocol is System.Exception. Note that this wrapper/holder holds more than just the struct/enum. It also holds the error message carried by the icerpc response header, and uses this error message to implement the Error trait/protocol.

I think result is a bit misleading. In Rust, if your function does not return a result it can not fail. In Slice you can still get a DispatchException.

I also find the syntax a bit clunky when there's no success type, e.g. result<(), Foo>.

We could do something similar to stream, but with error. We can enforce that there is only one and that it's the last (like stream). I also think it looks nicer.

op1() -> string
op2() -> error OpError
op2() -> (string, error OpError)

vs

op1() -> string
op2() -> result<(), OpError>
op2() -> result<string, error OpError>
pepone commented

op2() -> (string, error OpError)

I think this makes the tuple syntax a bit more complicated, the common case is just name: Type pairs. We shouldn't use a tuple when there is a single return element.

I'm not opposed to doing like @bentoi mentioned and using that same syntax, but with error on it's own following the return type: op2() -> (...myTuple...) error OpError.

Effectively just replacing the throws keyword with error basically (and changing what types can follow after it).

bentoi commented

I don't understand this argument. Result<Success, Failure> is common place, it's not just Rust. See for example: https://en.wikipedia.org/wiki/Result_type

None of the examples talk about exceptions. When I read an operation returning Result<S, F> I expect it returns a result. This won't be the case for the many exception based languages where it will return S and potentially throw F.

There is a widely used standard pattern called Result and yet you propose to use a different syntax? Why?

To me, it's a widely used pattern for functional languages to report errors, not for C++, Java, C#, Python, etc.

ZeroC.Slice.SliceException is the base class for Slice-mapped exception

Why does it need to be ZeroC.Slice? Can't it be in IceRpc.Slice instead?

You can certainly propose it, but I don't think we should do that. It would be both confusing and unworkable:

I don't find it's confusing. Errors are reported with exceptions in C++/Java/C#/Python, we generate an exception that holds the error specified on Slice operation. It's not difficult to explain.

what happens when 2 operations in 2 different Slice files return the same error?

Yes, it's a bit dodgy... I didn't fully think it through. Can't we generate the exception only if the first generated file? The exception would be in the same namespace as the error definition.

Why does it need to be ZeroC.Slice? Can't it be in IceRpc.Slice instead?

Because all the mechanics for encoding/decoding exceptions is largely shared with classes and the encoding/decoding of classes is naturally in the RPC-independent portion of Slice.

It's much easier to keep the exception (type) mapping RPC-independent, like today.

None of the examples talk about exceptions. When I read an operation returning Result<S, F> I expect it returns a result. This won't be the case for the many exception based languages where it will return S and potentially throw F

I see this as a feature, not a bug. The semantics is we return (don't throw) a custom error. Some languages provide built-in support for this Result type (Rust, Swift, Go, Kotlin, even C++ with std::expected as of C++23). Other languages like C# unfortunately don't have a built-in Result so we map it to exception.

I want to convey the error semantics (no inheritance, single exact type, and no throwing) and not the exception-sent-over-the-wire semantics provided by Ice (with inheritance, throwing and catching, and dynamic instance creation).

I think result is a bit misleading. In Rust, if your function does not return a result it can not fail. In Slice you can still get a DispatchException.

I don't find this argument convincing. A RPC can always fail, and the failure is RPC framework dependent. What we're describing with this result syntax is a custom error expressed in Slice.

Note that DispatchException is not a Slice-level concept. It used to be in the IceRPC core and is now moving to IceRPC + Slice.

We could do something similar to stream, but with error. We can enforce that there is only one and that it's the last (like stream). I also think it looks nicer.

I think we should use the standard pattern: Result. Not come up with a different syntax.

op2() -> error OpError

I feel like this almost implies that op2 will just always return an error I prefer op2() -> result<(), OpError>.

Yes, for the syntax I'm between either:

op() -> result<(), MyError>         // No return type, but can fail with a MyError.
op() -> result<string, MyError>     // Either succeeds and returns a string, or fails with a MyError
op() -> result<(a: bool, b: bool), MyError>

or

op() error MyError                  // No return type, but can fail with a MyError.
op() -> string error MyError        // Either succeeds and returns a string, or fails with a MyError
op() -> (a: bool, b: bool) error MyError

I think the semantics of Result better fit an RPC call (regardless of how it's mapped in languages),
but the error keyword is more ergonomic (no need for () return type, and cleaner to write return tuples with).

I think there's also a terminological difference here too.
With op() -> Result<A, B>, the entire Result is the return type. And that return type can be one of two discriminants.
With op() -> A error B, only A is the return type, and B is just an extra error type.
Not saying one is better than the other, just noting it.

bentoi commented

Re: SliceException vs DispacthException

So if I understand it correctly if I define a Slice1 operation that returns an error (defined as Slice1 exception), I'll have to catch an exception derived from SliceException but if I define it in Slice2 I will need to catch DispatchException<T>? There's no way to "catch all" errors from Slice2 operations?

There's no way to "catch all" errors from Slice2 operations?

That's correct per my proposal. Note that today, you can't catch all "remote" exceptions with a single catch block: you need to catch DispatchException (for IceRPC exceptions) and separately SliceException (the base of all Slice-defined exceptions).

If catching all dispatch exceptions in a single catch block is desired, we could make DispatchException<T> derive from DispatchException.

bentoi commented

From a C# application perspective, I find it would be simpler to understand if we just had a single exception for errors returned by a Slice operation.

Can't we replace DispatchException<T> with SliceException<T> (defined in IceRpc.Slice) and derive it from SliceException (defined in ZeroC.Slice)?

SliceException is the base class for errors returned by a Slice operation regardless of whether or not it's defined as a Slice1 exception or as a Slice2 constructed type.

Can't we replace DispatchException with SliceException (defined in IceRpc.Slice) and derive it from SliceException (defined in ZeroC.Slice)?

Please have a look at ZeroC.Slice.SliceException: it's all about encoding/decoding exceptions like Slice1 classes.

With Slice2, there is no such thing as a Slice-defined exception. So when you use Slice2 with your favorite RPC framework, you shouldn't need / see / be exposed to SliceException.

bentoi commented

With Slice2, there is no such thing as a Slice-defined exception. So when you use Slice2 with your favorite RPC framework, you shouldn't need / see / be exposed to SliceException.

Ok.

And there's really no way to get rid of SliceException from ZeroC.Slice? I might be missing something but it's mostly used for the implementation of DecodeUserException. Can't this method just return an object like the activator? The IceRpc+Slice integration would define an extension method that cast the object to an IceRpc.Slice.SliceException (instead of casting it in DecodeUserException).

I don't see how we could move SliceException to IceRpc.Slice. Slice exceptions and Slice classes are tied and belong to the RPC-independent portion of Slice.

The IceRpc+Slice integration would define an extension method that cast the object to an IceRpc.Slice.SliceException (instead of casting it in DecodeUserException).

Naturally, we can cast this object to a SliceException because the exception-class in question derives from SliceException. If SliceException was in IceRpc.Slice, then all the exception generated code would become IceRPC + Slice specific (in itself, a major pain). And since classes and exceptions are tied, we'd need to move the class encoding/decoding to IceRpc.Slice (even worse).

Moving the class/exception encoding/decoding out of ZeroC.Slice would be very difficult with our current setup where a single SliceEncoder/SliceDecoder ref struct is responsible for all encoding/decoding.

bentoi commented

I don't see how we could move SliceException to IceRpc.Slice. Slice exceptions and Slice classes are tied and belong to the RPC-independent portion of Slice.

The encoding of Slice exceptions is indeed part to the RPC-independent portion of Slice but to me the actual representation of the Slice exception could belong to IceRPC + Slice.

I was thinking something along these lines: https://github.com/icerpc/icerpc-csharp/compare/main...bentoi:icerpc-csharp:sliceexception?expand=1. I didn't actually test it because I don't know how to modify the slicec compiler to fix the generated code. It should use IceRpc.Slice for Slice files that contain an exception definition.

It's still possible I miss something...

It seems we're going off topic here. This issue/proposal is about Slice and replacing throws by Result. I'll create a separate issue in icerpc-csharp.

For the Result-error type with Slice2, there is no particular advantage to restrict the type specified by the user.

So I amend my proposal to allow any type as error: struct, enum, custom, Sequence, Dictionary, string, int32, etc., even proxy types. For example:

// custom error contains list of alternative greeters the caller may want to try
greet(name: string) -> Result<string, Sequence<GreeterProxy>>

I would however disallow optional types: it would be odd to get an error with value "not set".

This was replaced by #645 and #648

We're keeping the throws syntax as-is, but making it (and exceptions) Slice1 only.
Result will be used to express custom error types in Slice2, but as a normal Slice type.