In previous blogs, we have covered executing N1QL from JavaScript functions, processing documents through iterators, and manipulating data.
We now move onto handling errors from N1QL statements.
Error handling
When an error of any nature is encountered, the jsevaluator by default will halt the function execution and return an error:
In this particular case, the second INSERT will fail because key k10 already exists:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
[ { "code": 10109, "msg": "Error executing function 'doInsert' (n1ql:doInsert)", "reason": { "details": { "Code": " var q2 = N1QL('insert into b1 values(\\\"k10\\\", {\\\"f1\\\": 10});', {}, true);", "Exception": { "caller": "couchbase:2088", "cause": { "caller": "couchbase:1961", "code": 17012, "key": "dml.statement.duplicatekey", "message": "Duplicate Key: k10" }, "code": 12009, "icause": "Duplicate Key: k10", "key": "datastore.couchbase.DML_error", "message": "DML Error, possible causes include concurrent modification. Failed to perform INSERT on key k10", "retry": false }, "Location": "functions/n1ql.js:21", "Stack": " at doInsert (functions/n1ql.js:3:14)" }, "type": "Exceptions from JS code" } } ] |
You don’t have to stick to the default behaviour, JavaScript allows to catch errors and handle them:
In this case, the function doInsert() returns failure because key k10 already exists.
Return vs throw
The catch block can be used to handle the failure in whichever way is useful for your business logic.
The available options boil down to 1) take some evasive action and continue execution, 2) return early with success, and 3) return early with failure.
The difference between success and failure is simply using return to return a value as in the example above vs throw to return an error.
The difference between the two is important, because if you choose to return a result, the caller request will continue execution. While if you throw an error, the whole request will fail with the error, so do be mindful about which you want to use, because it will make the difference between your request working correctly and misbehaving.
Returned expression
The second thing to bear in mind is that, whether you return or throw an error, you may very well want to give meaningful information.
Consider the following code:
This will fail with something like:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
[ { "code": 10109, "msg": "Error executing function 'doInsert' (n1ql:doInsert)", "reason": { "details": { "Code": " throw err;", "Exception": { "caller": "couchbase:2088", "cause": { "caller": "couchbase:1961", "code": 17012, "key": "dml.statement.duplicatekey", "message": "Duplicate Key: k10" }, "code": 12009, "icause": "Duplicate Key: k10", "key": "datastore.couchbase.DML_error", "message": "DML Error, possible causes include concurrent modification. Failed to perform INSERT on key k10", "retry": false }, "Location": "functions/n1ql.js:6", "Stack": " at doInsert (functions/n1ql.js:3:17)" }, "type": "Exceptions from JS code" } } ] |
Much like the previous example, what we gather from this is that err is an object, and is returned in the reason field.
However, if you amend the throw statement like so:
The doInsert() function will now throw a string and not an object:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[ { "code": 10109, "msg": "Error executing function 'doInsert' (n1ql:doInsert)", "reason": { "details": { "Code": " throw \"failure\" + err;", "Exception": "failure {\"caller\":\"couchbase:2088\",\"cause\":{\"caller\":\"couchbase:1961\",\"code\":17012,\"key\":\"dml.statement.duplicatekey\",\"message\":\"Duplicate Key: k10\"},\"code\":12009,\"icause\":\"Duplicate Key: k10\",\"key\":\"datastore.couchbase.DML_error\",\"message\":\"DML Error, possible causes include concurrent modification. Failed to perform INSERT on key k10\",\"retry\":false}", "Location": "functions/n1ql.js:6" }, "type": "Exceptions from JS code" } } ] |
The err object is now marshalled into a string, and all of a sudden, your error is not as legible.
This applies to return too: in particular, by default err contains strings containing marshalled JSON objects.
For instance:
Returns:
1 2 3 4 5 6 |
[ { "message": "{\"caller\":\"couchbase:2088\",\"cause\":{\"caller\":\"couchbase:1961\",\"code\":17012,\"key\":\"dml.statement.duplicatekey\",\"message\":\"Duplicate Key: k10\"},\"code\":12009,\"icause\":\"Duplicate Key: k10\",\"key\":\"datastore.couchbase.DML_error\",\"message\":\"DML Error, possible causes include concurrent modification. Failed to perform INSERT on key k10\",\"retry\":false}", "stack": "Error\n at doInsert (functions/n1ql.js:3:17)" } ] |
If you want to obtain a nice returned value, you have to parse the error.message object before returning it:
Which yields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
[ { "caller": "couchbase:2088", "cause": { "caller": "couchbase:1961", "code": 17012, "key": "dml.statement.duplicatekey", "message": "Duplicate Key: k10" }, "code": 12009, "icause": "Duplicate Key: k10", "key": "datastore.couchbase.DML_error", "message": "DML Error, possible causes include concurrent modification. Failed to perform INSERT on key k10", "retry": false } ] |
And if you want to include the stack too, you have to construct a new object like so:
Which yields:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
[ { "caller": "couchbase:2088", "cause": { "caller": "couchbase:1961", "code": 17012, "key": "dml.statement.duplicatekey", "message": "Duplicate Key: k10" }, "code": 12009, "icause": "Duplicate Key: k10", "key": "datastore.couchbase.DML_error", "message": "DML Error, possible causes include concurrent modification. Failed to perform INSERT on key k10", "retry": false, "stack": "Error\n at doInsert (functions/n1ql.js:3:17)" } ] |
The moral of the story is: be mindful of how you return your error, and be mindful how you format it.
Conclusion
Over the course of several blogs, we have covered executing N1QL, processing documents and manipulating data, and handling errors.
Next in the series, we’ll move to prepared statements.