[squeak-dev] On the rejection of Promises due to errors

Robert Withers robert.withers at pm.me
Sun Jun 21 21:53:04 UTC 2020


Hi Jakob,

I also have a promises implementation for Squeak and Java, that was derived from ERights, which precedes the JavaScript impl, both by Mark Miller. They do NOT throw up exceptions but they do resolve the promise to a BrokenERef, encapsulating the exception.

You can load the following and run the tests.

> Installer ss project: 'Cryptography'; install: 'PromisesLocal'.

Then I converted the first code you presented as the following.

> promise := 1 eventual / 0.
>
> Number compile: 'methodWithTypo ^ self asstring'.
> promise := 1 eventual methodWithTypo.

They both resolve to BrokenERefs.

I got a little lost in capturing exceptions, within the Vat's event loop #processSends. I have tickled my implementation a little to try and get the Vat event thread to throw an exception, which presents a Debugger. I have been unable to pop up the Debugger on the error, but the promise does get smashed.

These Promises also pipelines the failure to subsequent message sends, with subsequent broken promises. So this also breaks with ZeroDivide.

> (1 eventual / 0) * 10.

Here are the immediate promise return and the smashed promise to a BrokenERef.

Kindly,
Robert

On 6/21/20 4:57 PM, Jakob Reschke wrote:

> Hi all,
>
> Tony has recently fixed our Promise implementation to make it more
> Promises/A+ compliant, thank you!
>
> In the discussion that lead to this fix [1], I already pointed out a
> difference between exceptions in Smalltalk and in JavaScript, where
> the Promises/A+ specification originates: Smalltalk exceptions can be
> resumed, while JavaScript exceptions cannot be resumed and always
> unroll the stack.
>
> The spec [2] says that if the onFulfilled or onRejected callback of a
> #then call on a promise throws an exception, then the promise returned
> by the #then call shall be rejected with the exception thrown.
>
> Our current Promise implementation matches this for the blocks
> supplied to #then:, #ifRejected: or #then:ifRejected, by catching all
> Errors in the blocks and rejecting the promise. But this does not
> allow a Squeak user to deal with exceptions in a debugger if they are
> signalled in the callbacks, because they are caught. The same also
> applies to #future promises. The latter are not really covered by the
> Promises/A+ spec (because it does not force the resolution or
> rejection of a promise that is not the result of a #then, and there is
> no #future in JavaScript), but futures exhibit the same problem of not
> being resumable in the debugger. Example:
>
> promise := 1 future / 0. "<= inspect it => promise is rejected,
> regardless of your actions in the debugger"
> Number compile: 'methodWithTypo  ^ self asstring'.
> promise := 1 future methodWithTypo. "<= inspect it => promise is
> rejected, no chance to fix the misspelling of asString in the debugger
> and proceed"
>
> I could imagine instead letting all exceptions pass during the future
> or callback block evaluation, and only reject the promise if the
> evaluation is eventually curtailed due to the exception (be it an
> Error or not, think of Warning or ModificationForbidden). Example
> expectations:
>
> promise := 1 future / 0. "<= inspect it, press Proceed in the
> debugger, => promise is resolved"
> promise := 1 future / 0. "<= inspect it, press Abandon in the
> debugger, => promise is rejected"
> promise := 1 future methodWithTypo. "<= inspect it, fix the typo of
> asString in the debugger, proceed, => promise is resolved with '1'"
>
> It could be done by fulfilling a Promise about aBlock similar to this:
>
> [ self resolveWith: aBlock value ]
>    on: Exception
>    do: [ :ex | | resumed |
>       resumed := false.
>       [ | result |
>       result := ex outer.
>       resumed := true.
>       ex resume: result]
>          ifCurtailed: [resumed ifFalse: [self future rejectWith: ex]]]
>
> (Find the current implementations here:
> Promise>>#fulfillWith:passErrors: and Promise>>#then:ifRejected:)
>
> Note that the #outer send would only trigger handlers in the
> Project/World loop, or the defaultAction of the exception. The #future
> in front of #rejectWith: is there to avoid curtailing the unwind block
> context of ifCurtailed: itself if there are further errors in the
> rejection callbacks of the promise. The behavior of non-local exits
> from unwind contexts is undefined in the Smalltalk ANSI standard (just
> like resume: or return: in a defaultAction, or not sending resume: or
> return: in an on:do: exception handler at all -- VA Smalltalk
> interprets that as resume, while Squeak does return, for example).
>
> This implementation would also allow all deferred Notifications to
> pass and not reject the promise. That is because true notifications
> just resume silently if they are not handled.
>
> promise := [Notification signal: 'hi there'. 42] future value. "<=
> inspect it => Expected: resolved with 42. Actual (today): it is
> needlessly rejected with Notification 'hi there'"
>
> Pressing Proceed in the debugger on officially non-resumable errors
> (which is possible) would also not reject the promise. But further
> errors/debuggers are likely to appear, of which one may eventually be
> used to abort the execution. If the execution finishes after
> repeatedly pressing Proceed, then fine, resolve the promise with
> whatever the outcome was.
>
> promise := [self error: 'Fatal error'. 42] future value. "<= inspect
> it, proceed after the so-called fatal error, => Expected: resolved
> with 42. Actual: there is no debugger, the promise is immediately
> rejected."
>
> promise := [1 / 0 + 3] future value. "<= Cannot be resumed/proceeded
> because if ZeroDivide is resumed, it will return the exception, and
> ZeroDivide does not understand +, which cannot be resumed without
> changing the code. So you'd have to curtail the block execution =>
> Expected: rejected with ZeroDivide or MessageNotUnderstood (depending
> on when you press Abandon or recompile the DoIt)."
>
> promise := [1 / 0 + 3] future value. "... or instead of changing the
> code or aborting, you could choose 'return entered value' in one of
> the debuggers, and thereby complete the evaluation of the block =>
> Expected: resolved with whatever you entered to return in the
> debugger"
>
> Promises with whenRejected:/ifRejected: callbacks would no longer
> swallow errors, and would only be rejected when the user aborts in the
> debuggers, or if the future execution catches errors by itself and
> converts them to rejected promises, so the future promise will also be
> rejected. This could pose a compatibility problem for existing code.
>
> promise := (1 future / 0) then: [:result | result + 3] ifRejected:
> [:reason | #cancelled]. "<= inspect it => Actual: resolved with
> #cancelled immediately. Expected with my proposed changes: it would
> first show the ZeroDivide debugger, which you can abandon to resolve
> with #cancelled, or proceed to a MessageNotUnderstood +. If you
> abandon the MNU, the promise would be rejected with the MNU, not
> #cancelled, in accordance with the Promises/A+ spec."
>
> How to get back a catch-all->reject-immediately future under these
> circumstances:
>
> promise := [[1 / 0] on: Error do: [:e | e return: (Promise new
> rejectWith: e)]] future value.
> promise := [1 future + 1 then: [:n | [n / 0] on: Error do: [:e | e
> return: (Promise new rejectWith: e)]] future value.
>
> We could also introduce a convenience constructor for
> immediately-rejected promises like in JavaScript: Promise rejected: e.
> Or a convenience exception handler: [...] rejectOn: Error.  Or [...]
> rejectIfCurtailed (the fullfill/then methods would probably use this
> as well).
>
> What do you think?
>
> As Tom Beckmann has already suggested in the last thread on the topic
> [1], I could also use a custom class of Promise to get just the
> behavior I want. But then I cannot solve it for the use of #future. At
> least not without patching something about the compiler in my package
> preamble... ;-)
>
> [1]
> http://lists.squeakfoundation.org/pipermail/squeak-dev/2020-April/208546.html
> [2]
> https://promisesaplus.com/
> Kind regards,
> Jakob
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.squeakfoundation.org/pipermail/squeak-dev/attachments/20200621/11294efe/attachment.html>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: BrokenERef.jpeg
Type: image/jpeg
Size: 13669 bytes
Desc: not available
URL: <http://lists.squeakfoundation.org/pipermail/squeak-dev/attachments/20200621/11294efe/attachment.jpeg>
-------------- next part --------------
A non-text attachment was scrubbed...
Name: PromiseERef.jpeg
Type: image/jpeg
Size: 14586 bytes
Desc: not available
URL: <http://lists.squeakfoundation.org/pipermail/squeak-dev/attachments/20200621/11294efe/attachment-0001.jpeg>


More information about the Squeak-dev mailing list