Skip to content

Commit c25d563

Browse files
committed
Support FunctionCallback returning an error.
If the error is a Valuer (like an *Exception), it will be thrown as a proper JS error, otherwise it becomes a thrown string.
1 parent 3a20035 commit c25d563

File tree

8 files changed

+179
-39
lines changed

8 files changed

+179
-39
lines changed

export_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
package v8go
66

77
// RegisterCallback is exported for testing only.
8-
func (i *Isolate) RegisterCallback(cb FunctionCallback) int {
8+
func (i *Isolate) RegisterCallback(cb FunctionCallbackWithError) int {
99
return i.registerCallback(cb)
1010
}
1111

1212
// GetCallback is exported for testing only.
13-
func (i *Isolate) GetCallback(ref int) FunctionCallback {
13+
func (i *Isolate) GetCallback(ref int) FunctionCallbackWithError {
1414
return i.getCallback(ref)
1515
}
1616

function_template.go

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,26 @@ import (
1515
// FunctionCallback is a callback that is executed in Go when a function is executed in JS.
1616
type FunctionCallback func(info *FunctionCallbackInfo) *Value
1717

18+
// FunctionCallbackWithError is a callback that is executed in Go when
19+
// a function is executed in JS. If a ValueError is returned, its
20+
// value will be thrown as an exception in V8, otherwise Error() is
21+
// invoked, and the string is thrown.
22+
type FunctionCallbackWithError func(info *FunctionCallbackInfo) (*Value, error)
23+
1824
// FunctionCallbackInfo is the argument that is passed to a FunctionCallback.
1925
type FunctionCallbackInfo struct {
2026
ctx *Context
2127
args []*Value
2228
this *Object
2329
}
2430

31+
// A ValuerError can be returned from a FunctionCallbackWithError, and
32+
// its value will be thrown as an exception in V8.
33+
type ValuerError interface {
34+
error
35+
Valuer
36+
}
37+
2538
// Context is the current context that the callback is being executed in.
2639
func (i *FunctionCallbackInfo) Context() *Context {
2740
return i.ctx
@@ -44,8 +57,21 @@ type FunctionTemplate struct {
4457
*template
4558
}
4659

47-
// NewFunctionTemplate creates a FunctionTemplate for a given callback.
60+
// NewFunctionTemplate creates a FunctionTemplate for a given
61+
// callback. Prefer using NewFunctionTemplateWithError.
4862
func NewFunctionTemplate(iso *Isolate, callback FunctionCallback) *FunctionTemplate {
63+
if callback == nil {
64+
panic("nil FunctionCallback argument not supported")
65+
}
66+
return NewFunctionTemplateWithError(iso, func(info *FunctionCallbackInfo) (*Value, error) {
67+
return callback(info), nil
68+
})
69+
}
70+
71+
// NewFunctionTemplateWithError creates a FunctionTemplate for a given
72+
// callback. If the callback returns an error, it will be thrown as a
73+
// JS error.
74+
func NewFunctionTemplateWithError(iso *Isolate, callback FunctionCallbackWithError) *FunctionTemplate {
4975
if iso == nil {
5076
panic("nil Isolate argument not supported")
5177
}
@@ -77,7 +103,7 @@ func (tmpl *FunctionTemplate) GetFunction(ctx *Context) *Function {
77103
// Note that ideally `thisAndArgs` would be split into two separate arguments, but they were combined
78104
// to workaround an ERROR_COMMITMENT_LIMIT error on windows that was detected in CI.
79105
//export goFunctionCallback
80-
func goFunctionCallback(ctxref int, cbref int, thisAndArgs *C.ValuePtr, argsCount int) C.ValuePtr {
106+
func goFunctionCallback(ctxref int, cbref int, thisAndArgs *C.ValuePtr, argsCount int) (rval C.ValuePtr, rerr C.ValuePtr) {
81107
ctx := getContext(ctxref)
82108

83109
this := *thisAndArgs
@@ -94,8 +120,19 @@ func goFunctionCallback(ctxref int, cbref int, thisAndArgs *C.ValuePtr, argsCoun
94120
}
95121

96122
callbackFunc := ctx.iso.getCallback(cbref)
97-
if val := callbackFunc(info); val != nil {
98-
return val.ptr
123+
val, err := callbackFunc(info)
124+
if err != nil {
125+
if verr, ok := err.(ValuerError); ok {
126+
return nil, verr.value().ptr
127+
}
128+
errv, err := NewValue(ctx.iso, err.Error())
129+
if err != nil {
130+
panic(err)
131+
}
132+
return nil, errv.ptr
133+
}
134+
if val == nil {
135+
return nil, nil
99136
}
100-
return nil
137+
return val.ptr, nil
101138
}

function_template_test.go

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -55,32 +55,84 @@ func TestFunctionTemplate_panic_on_nil_callback(t *testing.T) {
5555
func TestFunctionTemplateGetFunction(t *testing.T) {
5656
t.Parallel()
5757

58-
iso := v8.NewIsolate()
59-
defer iso.Dispose()
60-
ctx := v8.NewContext(iso)
61-
defer ctx.Close()
58+
t.Run("can_call", func(t *testing.T) {
59+
t.Parallel()
60+
61+
iso := v8.NewIsolate()
62+
defer iso.Dispose()
63+
ctx := v8.NewContext(iso)
64+
defer ctx.Close()
65+
66+
var args *v8.FunctionCallbackInfo
67+
tmpl := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value {
68+
args = info
69+
reply, _ := v8.NewValue(iso, "hello")
70+
return reply
71+
})
72+
fn := tmpl.GetFunction(ctx)
73+
ten, err := v8.NewValue(iso, int32(10))
74+
if err != nil {
75+
t.Fatal(err)
76+
}
77+
ret, err := fn.Call(v8.Undefined(iso), ten)
78+
if err != nil {
79+
t.Fatal(err)
80+
}
81+
if len(args.Args()) != 1 || args.Args()[0].String() != "10" {
82+
t.Fatalf("expected args [10], got: %+v", args.Args())
83+
}
84+
if !ret.IsString() || ret.String() != "hello" {
85+
t.Fatalf("expected return value of 'hello', was: %v", ret)
86+
}
87+
})
88+
89+
t.Run("can_throw_string", func(t *testing.T) {
90+
t.Parallel()
91+
92+
iso := v8.NewIsolate()
93+
defer iso.Dispose()
94+
95+
tmpl := v8.NewFunctionTemplateWithError(iso, func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
96+
return nil, fmt.Errorf("fake error")
97+
})
98+
global := v8.NewObjectTemplate(iso)
99+
global.Set("foo", tmpl)
100+
101+
ctx := v8.NewContext(iso, global)
102+
defer ctx.Close()
103+
104+
ret, err := ctx.RunScript("(() => { try { foo(); return null; } catch (e) { return e; } })()", "")
105+
if err != nil {
106+
t.Fatal(err)
107+
}
108+
if !ret.IsString() || ret.String() != "fake error" {
109+
t.Fatalf("expected return value of 'hello', was: %v", ret)
110+
}
111+
})
112+
113+
t.Run("can_throw_exception", func(t *testing.T) {
114+
t.Parallel()
115+
116+
iso := v8.NewIsolate()
117+
defer iso.Dispose()
118+
119+
tmpl := v8.NewFunctionTemplateWithError(iso, func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
120+
return nil, v8.NewError(iso, "fake error")
121+
})
122+
global := v8.NewObjectTemplate(iso)
123+
global.Set("foo", tmpl)
124+
125+
ctx := v8.NewContext(iso, global)
126+
defer ctx.Close()
62127

63-
var args *v8.FunctionCallbackInfo
64-
tmpl := v8.NewFunctionTemplate(iso, func(info *v8.FunctionCallbackInfo) *v8.Value {
65-
args = info
66-
reply, _ := v8.NewValue(iso, "hello")
67-
return reply
128+
ret, err := ctx.RunScript("(() => { try { foo(); return null; } catch (e) { return e; } })()", "")
129+
if err != nil {
130+
t.Fatal(err)
131+
}
132+
if !ret.IsNativeError() || !strings.Contains(ret.String(), "fake error") {
133+
t.Fatalf("expected return value of Error('hello'), was: %v", ret)
134+
}
68135
})
69-
fn := tmpl.GetFunction(ctx)
70-
ten, err := v8.NewValue(iso, int32(10))
71-
if err != nil {
72-
t.Fatal(err)
73-
}
74-
ret, err := fn.Call(v8.Undefined(iso), ten)
75-
if err != nil {
76-
t.Fatal(err)
77-
}
78-
if len(args.Args()) != 1 || args.Args()[0].String() != "10" {
79-
t.Fatalf("expected args [10], got: %+v", args.Args())
80-
}
81-
if !ret.IsString() || ret.String() != "hello" {
82-
t.Fatalf("expected return value of 'hello', was: %v", ret)
83-
}
84136
}
85137

86138
func TestFunctionCallbackInfoThis(t *testing.T) {

isolate.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type Isolate struct {
2121

2222
cbMutex sync.RWMutex
2323
cbSeq int
24-
cbs map[int]FunctionCallback
24+
cbs map[int]FunctionCallbackWithError
2525

2626
null *Value
2727
undefined *Value
@@ -55,7 +55,7 @@ func NewIsolate() *Isolate {
5555
})
5656
iso := &Isolate{
5757
ptr: C.NewIsolate(),
58-
cbs: make(map[int]FunctionCallback),
58+
cbs: make(map[int]FunctionCallbackWithError),
5959
}
6060
iso.null = newValueNull(iso)
6161
iso.undefined = newValueUndefined(iso)
@@ -112,7 +112,7 @@ func (i *Isolate) apply(opts *contextOptions) {
112112
opts.iso = i
113113
}
114114

115-
func (i *Isolate) registerCallback(cb FunctionCallback) int {
115+
func (i *Isolate) registerCallback(cb FunctionCallbackWithError) int {
116116
i.cbMutex.Lock()
117117
i.cbSeq++
118118
ref := i.cbSeq
@@ -121,7 +121,7 @@ func (i *Isolate) registerCallback(cb FunctionCallback) int {
121121
return ref
122122
}
123123

124-
func (i *Isolate) getCallback(ref int) FunctionCallback {
124+
func (i *Isolate) getCallback(ref int) FunctionCallbackWithError {
125125
i.cbMutex.RLock()
126126
defer i.cbMutex.RUnlock()
127127
return i.cbs[ref]

isolate_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func TestCallbackRegistry(t *testing.T) {
8282

8383
iso := v8.NewIsolate()
8484
defer iso.Dispose()
85-
cb := func(*v8.FunctionCallbackInfo) *v8.Value { return nil }
85+
cb := func(*v8.FunctionCallbackInfo) (*v8.Value, error) { return nil, nil }
8686

8787
cb0 := iso.GetCallback(0)
8888
if cb0 != nil {

promise.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,18 @@ func (p *Promise) Result() *Value {
9494
// The default MicrotaskPolicy processes them when the call depth decreases to 0.
9595
// Call (*Context).PerformMicrotaskCheckpoint to trigger it manually.
9696
func (p *Promise) Then(cbs ...FunctionCallback) *Promise {
97+
cbwes := make([]FunctionCallbackWithError, len(cbs))
98+
for i, cb := range cbs {
99+
cb := cb
100+
cbwes[i] = func(info *FunctionCallbackInfo) (*Value, error) {
101+
return cb(info), nil
102+
}
103+
}
104+
105+
return p.ThenWithError(cbwes...)
106+
}
107+
108+
func (p *Promise) ThenWithError(cbs ...FunctionCallbackWithError) *Promise {
97109
var rtn C.RtnValue
98110
switch len(cbs) {
99111
case 1:
@@ -117,6 +129,12 @@ func (p *Promise) Then(cbs ...FunctionCallback) *Promise {
117129
// Catch invokes the given function if the promise is rejected.
118130
// See Then for other details.
119131
func (p *Promise) Catch(cb FunctionCallback) *Promise {
132+
return p.CatchWithError(func(info *FunctionCallbackInfo) (*Value, error) {
133+
return cb(info), nil
134+
})
135+
}
136+
137+
func (p *Promise) CatchWithError(cb FunctionCallbackWithError) *Promise {
120138
cbID := p.ctx.iso.registerCallback(cb)
121139
rtn := C.PromiseCatch(p.ptr, C.int(cbID))
122140
obj, err := objectResult(p.ctx, rtn)

promise_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package v8go_test
66

77
import (
8+
"errors"
89
"testing"
910

1011
v8 "rogchap.com/v8go"
@@ -110,6 +111,36 @@ func TestPromiseRejected(t *testing.T) {
110111
}
111112
}
112113

114+
func TestPromiseThenCanThrow(t *testing.T) {
115+
t.Parallel()
116+
117+
iso := v8.NewIsolate()
118+
defer iso.Dispose()
119+
ctx := v8.NewContext(iso)
120+
defer ctx.Close()
121+
122+
res, _ := v8.NewPromiseResolver(ctx)
123+
124+
promThenVal := res.GetPromise().ThenWithError(func(info *v8.FunctionCallbackInfo) (*v8.Value, error) {
125+
return nil, errors.New("faked error")
126+
})
127+
promThen, err := promThenVal.AsPromise()
128+
if err != nil {
129+
t.Fatalf("AsPromise failed: %v", err)
130+
}
131+
132+
val, _ := v8.NewValue(iso, "foo")
133+
res.Resolve(val)
134+
135+
if s := promThen.State(); s != v8.Rejected {
136+
t.Fatalf("unexpected state for Promise, want Rejected (%v) got: %v", v8.Rejected, s)
137+
}
138+
139+
if result := promThen.Result(); result.String() != "faked error" {
140+
t.Errorf("expected the Promise result to match the resolve value, but got: %s", result)
141+
}
142+
}
143+
113144
func TestPromiseThenPanic(t *testing.T) {
114145
t.Parallel()
115146

v8go.cc

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,12 @@ static void FunctionTemplateCallback(const FunctionCallbackInfo<Value>& info) {
311311
args[i] = tracked_value(ctx, val);
312312
}
313313

314-
ValuePtr val =
314+
goFunctionCallback_return retval =
315315
goFunctionCallback(ctx_ref, callback_ref, thisAndArgs, args_count);
316-
if (val != nullptr) {
317-
info.GetReturnValue().Set(val->ptr.Get(iso));
316+
if (retval.r1 != nullptr) {
317+
iso->ThrowException(retval.r1->ptr.Get(iso));
318+
} else if (retval.r0 != nullptr) {
319+
info.GetReturnValue().Set(retval.r0->ptr.Get(iso));
318320
} else {
319321
info.GetReturnValue().SetUndefined();
320322
}

0 commit comments

Comments
 (0)