Fork me on GitHub
#clojurescript
<
2023-11-21
>
itaied08:11:59

Hey guys, I'm using cljs.test (in node) to test async code. My tests look something like that:

(deftest single-anchor-created
  (async done
         (go
           (let [before {}
                 after {:id (str (random-uuid))
                        :extends ["ID"]}]

             (<! (match-trees before after))

             (done)))))
match-trees makes some HTTP calls to remote services (handling promises with <p!). When an error occurs, I'm getting this generic misinformed error:
/builds/dev/dev-apps/app/.shadow-cljs/builds/test/dev/out/cljs-runtime/cljs/core.cljs:11623
  (let [e (js/Error. message)]
          ^
Error: Promise error
    at new cljs$core$ExceptionInfo (/builds/dev/dev-apps/app/.shadow-cljs/builds/test/dev/out/cljs-runtime/cljs/core.cljs:11623:11)
    at Function.cljs$core$IFn$_invoke$arity$3 (/builds/dev/dev-apps/app/.shadow-cljs/builds/test/dev/out/cljs-runtime/cljs/core.cljs:11650:1)
    at /builds/dev/dev-apps/app/.shadow-cljs/builds/test/dev/out/cljs-runtime/cljs/core/async/interop.cljs:26:26
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
error Command failed with exit code 1.
I'm compiling with shadow-cljs and running with node: yarn shadow-cljs compile test && node out/node-tests.js
:test {:target    :node-test
       :output-to "out/node-tests.js"}
How can I configure something to print the message of the promise error? I did add some (.catch promise) to print some errors, but it's not ideal to write it everywhere in my code...

thheller08:11:58

you are going to need to handle errors in some way. if you are only using core.async for promise handling I strongly suggest to drop that entirely and just use promises directly

thheller08:11:27

core.async go code is really annoying to debug

itaied08:11:04

from js world (specifically jest) I remember that a test that throws an error (`async-await`) will print the error message

itaied08:11:10

Is there some equivalent?

thheller08:11:59

no, as the test handler has no knowledge of your channels or promises in any way

itaied08:11:36

So I need to wrap each call with try/catch or .catch? Maybe a wrapper around the deftest?

thheller08:11:17

every call no, where you are interfacing with the promise. one place should be enough.

itaied08:11:30

I have just went over the project again, there are plenty of promises. Do you mean that each call to a promise should add a .catch? I don't understand how will it be a single place

thheller09:11:57

well I cannot see your code, so I don't know how you are interfacing with promises in the first place

itaied09:11:31

When having a go block, a rejected promise should throw it up to the caller, right?

thheller09:11:00

core.async puts this Promise error onto the channel, it then throws it when reading from the channel. so if you don't have a try/catch anywhere then it just makes it into your go code

thheller09:11:08

and ends up as an uncaught exception

thheller09:11:36

a go block doesn't have a caller, so no

itaied09:11:21

I think we have deviated a bit into core.async, there's something I probably don't undertand.

(defn c []
  (go (<p! rejected-promise)))

(defn b []
  (go (<! (c))))

(defn a []
  (go (<! (b))))
if I wrap a in try/catch, the rejected promise will be caught?
(defn a []
  (try (go (<! (b)))
     (catch :default e (println e)))

itaied09:11:08

It's just a nightmare to use only promises, we have dependent calls, and it's a callback (`.then`) hell...

thheller09:11:50

makes it look more like async/await in JS

thheller09:11:00

(defn c []
  (go (<p! rejected-promise)))

(defn b []
  (go (let [res (<! (c))]
        (js/console.log "result from c" res)
        res)))

(defn a []
  (go (<! (b))))

thheller09:11:52

try that to check what you result you get from c. I'm fairly sure thats just nil, so the exception is lost in c already, it doesn't travel further

thheller09:11:14

promises chain together to sort of carry the exception through, core.async does not

itaied09:11:43

OK I see. Regarding js-await, it does create nested calls (pyramid like). I guess we cannot achieve something like js async await:

const f = async () => {
  const a = await promise()
  const b = await promise(a)
  const c = await promise (b)
}
where the caller of f:
try{ await f() } catch { ... }

thheller09:11:22

as I said .. promises chain. so yes you can

itaied09:11:31

something like that:

(defn my-async-fn [foo]
  (let [the-result (js-await (promise-producing-call foo))
        the-result-2 (js-await (doing-something-with-the-result the-result))]
    (calc the-result-2))
  
(try (js-await (my-async-fn))
     (catch ...))

thheller09:11:42

thats not how you use that

itaied09:11:11

Yes I understand, I'm saying, using js-await does create nested code

thheller09:11:16

(defn my-async-fn [foo]
  (js-await [the-result (promise-producing-call foo)]
    (js-await [the-result-2 (doing-something-with-the-result the-result)]
      (calc the-result-2)
      )))

(js-await [the-result (my-async-fn)]
  
  (catch err
    ...))

thheller09:11:39

await also creates nested code, its just visually not nested. semantically it still is

itaied09:11:49

correct. so using js-await require a single catch block?

thheller09:11:09

I cannot answer that without seeing what you are doing in the code šŸ˜›

thheller09:11:15

in the example above yes

itaied09:11:38

ok ill give it a shot. Thanks!

p-himik10:11:33

IMO this code that you posted

const f = async () => {
  const a = await promise()
  const b = await promise(a)
  const c = await promise (b)
}
Is even easier to write with just .then:
(defn f []
  (-> (promise)
      (.then promise)
      (.then promise))
  ;; To make it behave the same as the JS fn above.
  (js/Promise.resolve))

thheller10:11:21

> Because the JS fn above doesn't return anything

thheller10:11:38

that is incorrect, it is a async fn, therefore it ALWAYS returns a Promise

p-himik10:11:45

Ah yeah, my bad, updated.

thheller10:11:10

but I agree, .then is totally fine there

thheller10:11:33

that adjustments makes no sense, just return the original promise chain šŸ˜›

p-himik10:11:05

I just wanted to make it behave in the exactly same way. :) So it's the JS function's fault it doesn't return the original.

thheller10:11:21

I'm fairly certain that the async fn above only resolves once all the awaits are done, even if with no own return values. yours resolves immediately

thheller10:11:23

(defn f []
  (-> (promise)
      (.then promise)
      (.then promise)
      (.then (fn [dismissed-result] js/undefined))))

p-himik10:11:23

Dang it. Yeah.

itaied10:11:34

The problems I'm trying to overcome are: ā€¢ A single catch block in the top scope ā€¢ Using multiple promises results like:

const a = await foo()
const b = await bar(a)
const c = await baz(a, b)

p-himik10:11:47

If you chain promises (returning a new promise from a .then handler would also be chained), you only have to use .catch once. When you break a chain, you have to add another .catch. To catch any errors thrown while a promise is being handled when there's no corresponding .catch, you can listen to the unhandledrejection event globally.

p-himik10:11:35

The JS code above can be written as:

(let [ap (foo)
      bp (.then ap bar)
      cp (.then (js/Promise.all [ap bp])
                (fn [[a b]]
                  (baz a b)))]
  (.then cp
         (fn [c]
           ...)))
Alternatively:
(-> (foo)
    (.then (fn [a]
             (.then (bar a) (fn [b] [a b])))
    (.then (fn [[a b]]
             (baz a b)))
    (.then (fn [c]
             ...)))
In both cases, a single .catch at the end will be enough to catch any error that was thrown during any of the mentioned calls. But just as with the case of some top-level (try ... (catch ...)), you might lose a bit of helpful context. Also note that chaining .then after .catch with handle the result of the catch handler as if it were a regular value. So if you want for a caught exception to propagate all the way through to the top, don't use .catch in the middle or rethrow the error, perhaps with additional context.

thheller11:11:16

in my mind js async/await works exactly like the js-await macro looks. as in execute something, then execute the body or catch. there is no magic chaining going on in JS either, so technically the above translation is wrong

thheller11:11:36

(.then (foo)
  (fn [a]
    (.then (bar a)
      (fn [b]
        (.then (baz a b)
          (fn [c]
            ...
            ))))))
is what actually happens basically

thheller11:11:40

but nobody likes that nesting šŸ˜›

p-himik11:11:26

Not sure I follow this time. In which scenario my code above wouldn't behave in the same way? > but nobody likes that nesting Yeah, that's why I immediately shied away from that. :D

thheller11:11:22

yes, but then you have to do extra work to shuttle the results around. if you just have functions that capture those its fine.

p-himik11:11:36

Right. I was translating only the behavior, without any attempts to make the translation mechanically perfect.

itaied11:11:05

ok guys I think ill opt out to .then , thanks

thheller11:11:19

note that you can use .then and js-await interchangeably since its just raw promises without any kind of wrapper. both work seamlessly together

šŸ‘ 1
thheller11:11:02

(-> (js-await [thing (foo)]
      (do-something thing))
    (.then something-else))

thheller11:11:18

not that you'd do something like this, but it is fine to do

Chris McCormick12:11:54

@U057T406Y15 in my opinion you could achieve your goal and get much nicer looking code by using the promesa library.

const a = await foo()
const b = await bar(a)
const c = await baz(a, b)
becomes:
(:require [promesa.core :as p])

(p/catch
  (p/let [a (foo)
          b (bar a)
          c (baz a b)]
     ... resolve test condition here ...
    )
    (fn [err] ... do something with error ...))

Chris McCormick12:11:24

If you literally just want to catch the top level error so you can print it then there is a feature in Node to do that:

process.on('unhandledRejection', error => {
  console.log('unhandledRejection', error.message);
});
Any unhandled Promise rejection will arrive there.

itaied12:11:55

thanks @UUSQUGUF3, I have just made the project to work so I won't try it right now, but I have written it aside (the promesa)

šŸ‘ 1
vraid14:11:26

Anyone familiar with https://github.com/thi-ng/geom/blob/feature/no-org/org/examples/gl/webgl.org#example-1-2d-polygons and/or webgl here? I'd like to draw some polygons defined in code with one color per vertex, but all the examples i can find either use a uniform color for the entire shape, or forgo colors entirely and use textures. Any help or pointers in the right direction would be appreciated.

p-himik15:11:36

Definitely not an expert, but I'm pretty sure you have to either write a shader that does that or use a texture.

vraid15:11:59

Any idea where to start on shaders? I only had a brief look as i've never written any before (all previous openGL work was with vertex and index buffers), and couldn't quite figure out how to structure them or access variables

p-himik15:11:26

Hold on, I'm actually probably wrong, checking.

p-himik15:11:56

The docs for aren't great. :(

p-himik16:11:11

And I'm pretty sure it has a bug. I can replace :fixed-color with :colors, according to the sources. But seems that I have to make it a vector of vectors, and it fails later when tries to fill a buffer with all those values.

p-himik16:11:23

@U0552GV2X32 Is this the expected image?

p-himik16:11:37

Just created an issue for that also shows how to implement the above: https://github.com/thi-ng/geom/issues/99

vraid21:11:05

@U2FRKM4TW Yes, cool! Technically i'm drawing polygons each of their own color, but your example is the more general case

vraid21:11:16

I'll see if i can get it to work now, thanks a lot for the help!

vraid21:11:27

Glad to know i'm not crazy for struggling a bit with it