New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[RFC] Communicating failures in our APIs #3637
Comments
a weird idea i've not put much thought into: How it would workclass Editor<ResultType = unknown> {
// Editor has a `result` property. by default it's type is `unknown` and it's set to undefined
result: ResultType = undefined
// we have utilities that override this property, and return `this` with the type overridden
protected returnOk<T>(value: T): Editor<OkResult<T>> {
(this as any).result = Result.ok(value)
return this as Editor<OkResult<T>>
}
protected returnErr<E>(error: E): Editor<ErrResult<E>> {
(this as any).result = Result.err(error)
return this as Editor<ErrResult<E>>
}
// Instead of just `return this`, operations that can fail use the helpers when returning.
// the inferred return type here is `Editor<Result<Shape[], string>>`
createShapes() {
if (tooManyShapes) {
return this.returnErr('too many shapes')
}
// ...
return this.returnOk(createdShapes)
}
} Usage// if i don't care about results:
editor.createShapes().updateShapes().whatever
// if i do care about results:
const result = editor.createShapes().result
if (result.ok) {
console.log('created shapes:', result.value)
} Pros
Cons
const foo = editor.createShapes()
const bar = editor.updateShapes()
if (foo.result.ok) {
// oh no, this is actually the `Result` from `updateShapes`
} |
Yeah, it also crossed my mind 😄 It reminds me of working with device statuses in low level programming. |
Here's yet another idea to mull over. For Paper we did a lot of this:
this would create a 'call stack', as we called it (really, a transaction), that would basically handle the operations and rollback if an error was thrown while executing. We kind of already do this with tldraw in the form of So, all this is to say, perhaps the communication of failures is maybe a mix of what you're suggesting @MitjaBezensek. We could have lower/mid-level functions throw errors. But we might communicate to consumers of the SDK to start using "safe" wrappers like Definitely worth a meeting to discuss at length!
and yeah, for this idea - another worry would be in multiplayer where |
I'd like to limit this RFC just to the low / mid level operations. Users won't just switch to higher level things, you might need more granularity in many cases so people will continue using them, and we'll also need a solution for the low / mid level if we want to build the higher level things (you only know about the issues where they happen). Happy to discuss the higher level things as well, just so we don't move in a direction that would be counterproductive in the future.
Remote changes don't go through these APIs, so I think it would probably be fine 🤷♂️ 😄 |
Problem
Our API currently does not do a good job communicating failures to the consumers. This can lead to using the API in an overly optimistic way, thinking that the API call succeeded.
The only way to prevent these kinds of issues at the moment is to do guard checks before / after calling the API, which is problematic since the are not really discoverable. You need to dig into the source code to see the ways in which API calls can fail.
Another problem is that the before checks are also not enough due to the multiplayer nature of the library. The state can change between the time you do the check and the time you perform the action.
For an example see the issue we currently have with creating shapes when max shapes has already been reached. The consumers of
createShapes
have no way of knowing that this problem occurred, since thecreateShapes
method returns the editor instance in any case. This caused issues for us, even though we are a lot more familiar with our APIs, but it is probably worse for somebody that is not as familiar with the library.Options
Do nothing
We can decide to avoid addressing this problem and just continue swallowing most of the errors. Interestingly we do throw errors in a couple of places.
Throwing errors
API would throw errors as they are encountered. We could define different error types so that consumers get some additional information about what went wrong.
Pros:
Cons:
Returning errors
Similar to throwing errors, but we instead return them. This helps with the DX as we can more easily expose the types of errors that can occur in the return types of the methods.
Pro:
Cons:
Returning statuses
We could only return statuses like a
true
/false
. Could also be a list ids of the created shapes or null / undefined if something went wrong,…Pros:
Cons:
Result object
Using a Result type (similar to what we already use internally in some places). I did a quick exploration of this for
createShape
andcreateShapes
methods here. I really like this pattern. If this was just for internal purposes I'd definitely go with this, but might feel a bit much to use it for the public facing API?Pros:
Cons:
The text was updated successfully, but these errors were encountered: