docs
Loading...
Searching...
No Matches
Error.h
Go to the documentation of this file.
1///
2/// Copyright (C) 2026 Ultralight, Inc. All rights reserved.
3/// A license is required for commercial use. https://ultralig.ht
4///
5#pragma once
6#include <Ultralight/CAPI/CAPI_JSAPI.h>
7#include <Ultralight/CAPI/CAPI_JSValue.h>
8#include <Ultralight/CAPI/CAPI_String.h>
9#include <Ultralight/detail/Expected.h>
10#include <Ultralight/js/detail/Swallow.h>
11
12#include <string>
13#include <type_traits>
14#include <utility>
15
16// The Expected/Unexpected implementation is shared across the typed header families and
17// lives in <Ultralight/detail/Expected.h>; the js/ names below alias it. The reader-facing
18// story lives on the js::Result alias doc at the end of this header.
19
20namespace ultralight {
21namespace js {
22
23/// \cond INTERNAL
24namespace detail {
25struct ErrorAccess;
26
27// The details of a thrown value, read once and released on destruction. Everything reads as
28// empty when there's no exception or its page is gone.
29class ExceptionDetails {
30 public:
31 explicit ExceptionDetails(ULJSValue exception) {
32 if (exception && ulJSValueIsAlive(exception))
33 ulJSValueGetErrorDetails(exception, &raw_);
34 }
35 ~ExceptionDetails() { ulJSErrorDetailsRelease(&raw_); }
36 ExceptionDetails(const ExceptionDetails&) = delete;
37 ExceptionDetails& operator=(const ExceptionDetails&) = delete;
38
39 const ULJSErrorDetails& raw() const { return raw_; }
40
41 static std::string Text(ULString s) {
42 return s ? std::string(ulStringGetData(s), ulStringGetLength(s)) : std::string();
43 }
44
45 private:
46 ULJSErrorDetails raw_ {};
47};
48} // namespace detail
49/// \endcond
50
51///
52/// The type of a JavaScript error, matching the standard error constructors.
53///
54enum class ErrorType : unsigned {
55 Error = 0, ///< A plain Error, or an error type with no dedicated constant.
56 TypeError,
58 SyntaxError,
60};
61
62///
63/// An error from a JavaScript operation.
64///
65/// The library never throws C++ exceptions. Operations that can fail return a js::Result, and
66/// its Error is one of four kinds:
67///
68/// - **A JavaScript exception** (is_exception() is true): script threw, or the call was
69/// invalid in JavaScript (eg, calling something that isn't a function). exception() holds
70/// the thrown value, and type(), stack(), source_url(), line(), and column() describe it.
71/// - **Empty handle** (is_empty() is true): the Value or Context you called it on holds nothing
72/// (see js::Value). This usually means a bug in your code.
73/// - **Page gone** (is_page_gone() is true): the handle's page is gone (see js::Value).
74/// - **A native error** (all three are false): an error created in C++, by you (TypeError(),
75/// RangeError(), Make()) or by the library (eg, when To() gets the wrong type).
76///
77/// A page can go away at any time, so a page-gone error usually needs no report. Report the rest
78/// with message():
79///
80/// ```
81/// js::Result<std::string> title = value.ToString();
82/// if (!title) {
83/// if (title.error().is_page_gone())
84/// return; // the page went away; nothing to report
85/// Log(title.error().message()); // script threw or the Value was empty: report it
86/// }
87/// ```
88///
89/// A bound function throws an Error into the page by returning it in a js::Result. Use
90/// WithCode() to give it a `code` property, so page script can check the kind of error without
91/// parsing the message.
92///
93/// ## Error Codes
94///
95/// The TypeErrors the library creates have a `code` property (read it with code() in native
96/// code, or `error.code` in page script). Check the code rather than the message, since message
97/// text is written for people and can change in any release. The codes below stay the same
98/// (new ones may be added).
99///
100/// | Code | Meaning |
101/// |----------------------|---------------------------------------------------------------------|
102/// | `ULJS_BAD_ARG` | A value doesn't have the expected type, or its text doesn't parse |
103/// | `ULJS_LOSSY` | A number doesn't fit the parameter's type (Strict diagnostics only) |
104/// | `ULJS_EXTRA_ARGS` | A call passed more arguments than the binding takes (Strict only) |
105/// | `ULJS_DETACHED` | The native object or API behind a binding or instance is gone |
106/// | `ULJS_NOT_INSTANCE` | A class method was called on an object that isn't an instance |
107/// | `ULJS_NO_CTOR` | Page script used `new` on a class that has no constructor |
108/// | `ULJS_CROSS_CONTEXT` | A value was used on a page it can't be shared with (see js::Value) |
109///
110/// @note When JavaScript diagnostics are on (see js::Diagnostics; they're on by default in
111/// developer mode), an Error that holds a JavaScript exception logs it if it's
112/// destroyed before anything reads it (any accessor, such as message(), counts). So a
113/// dropped Result never hides a script error during development.
114///
115class Error {
116 public:
117 ///
118 /// Create a page-gone error (is_page_gone() returns true).
119 ///
120 /// @return Returns the new Error.
121 ///
122 static Error PageGone() { return Error(Kind::kPageGone); }
123
124 ///
125 /// Create an error from a JavaScript exception, taking ownership of the handle.
126 ///
127 /// @param exception The thrown value. The Error destroys the handle when it's done with it.
128 ///
129 /// @return Returns the new Error.
130 ///
131 static Error AdoptException(ULJSValue exception) {
132 Error error(Kind::kException);
133 error.exception_ = exception;
134 return error;
135 }
136
137 ///
138 /// Create a native TypeError.
139 ///
140 /// @param message The description, returned by message().
141 ///
142 /// @return Returns the new Error.
143 ///
144 static Error TypeError(std::string message) {
145 return Error(ErrorType::TypeError, std::move(message));
146 }
147
148 ///
149 /// Create a native RangeError.
150 ///
151 /// @param message The description, returned by message().
152 ///
153 /// @return Returns the new Error.
154 ///
155 static Error RangeError(std::string message) {
156 return Error(ErrorType::RangeError, std::move(message));
157 }
158
159 ///
160 /// Create a native error of any type.
161 ///
162 /// @param type The error type, returned by type().
163 ///
164 /// @param message The description, returned by message().
165 ///
166 /// @return Returns the new Error.
167 ///
168 static Error Make(ErrorType type, std::string message) {
169 return Error(type, std::move(message));
170 }
171
172 ///
173 /// Move constructor (`other` no longer holds the exception).
174 ///
175 Error(Error&& other) noexcept
176 : kind_(other.kind_), type_(other.type_), exception_(other.exception_),
177 message_(std::move(other.message_)), code_(std::move(other.code_)),
178 observed_(other.observed_) {
179 other.exception_ = nullptr;
180 }
181
182 ///
183 /// Move assignment (`other` no longer holds the exception).
184 ///
185 Error& operator=(Error&& other) noexcept {
186 if (this != &other) {
187 ReleaseException();
188 kind_ = other.kind_;
189 type_ = other.type_;
190 exception_ = other.exception_;
191 message_ = std::move(other.message_);
192 code_ = std::move(other.code_);
193 observed_ = other.observed_;
194 other.exception_ = nullptr;
195 }
196 return *this;
197 }
198
199 ///
200 /// Copy constructor (takes its own reference to the exception, if any).
201 ///
202 Error(const Error& other)
203 : kind_(other.kind_), type_(other.type_),
204 exception_(other.exception_ ? ulCreateJSValueRef(other.exception_) : nullptr),
205 message_(other.message_), code_(other.code_) {
206 other.observed_ = true;
207 }
208
209 ///
210 /// Copy assignment (takes its own reference to the exception, if any).
211 ///
212 Error& operator=(const Error& other) {
213 if (this != &other)
214 *this = Error(other);
215 return *this;
216 }
217
218 ///
219 /// Destructor (logs the exception first if nothing read it; see the class notes).
220 ///
221 ~Error() { ReleaseException(); }
222
223 ///
224 /// Add a code that page script gets as the error's `code` property when it's thrown.
225 ///
226 /// ```
227 /// return js::Unexpected(js::Error::TypeError("bad slot").WithCode("BAD_SLOT"));
228 /// ```
229 ///
230 /// @param code The code, returned by code().
231 ///
232 /// @return Returns the Error with the code added.
233 ///
234 Error WithCode(std::string code) && {
235 code_ = std::move(code);
236 return std::move(*this);
237 }
238
239 ///
240 /// Whether or not this error holds a JavaScript exception.
241 ///
242 bool is_exception() const {
243 observed_ = true;
244 return kind_ == Kind::kException;
245 }
246
247 ///
248 /// Whether or not this error means the handle's page is gone.
249 ///
250 bool is_page_gone() const {
251 observed_ = true;
252 return kind_ == Kind::kPageGone;
253 }
254
255 ///
256 /// Whether or not this error means the handle holds nothing (see the class notes).
257 ///
258 bool is_empty() const {
259 observed_ = true;
260 return kind_ == Kind::kEmpty;
261 }
262
263 ///
264 /// Get the JavaScript exception (NULL if is_exception() is false).
265 ///
266 /// @note The Error owns the handle. Call ulCreateJSValueRef() to keep it longer.
267 ///
268 ULJSValue exception() const {
269 observed_ = true;
270 return exception_;
271 }
272
273 ///
274 /// Get the error's type.
275 ///
276 /// For a JavaScript exception, this is the class of the thrown error. It reads as
277 /// ErrorType::Error for any other class, for a thrown value that isn't an error object, and
278 /// once the exception's page is gone.
279 ///
280 /// @note For a thrown value that isn't an error object, this converts it to a string, which
281 /// can run script.
282 ///
283 ErrorType type() const {
284 observed_ = true;
285 if (kind_ != Kind::kException)
286 return type_;
287 return static_cast<ErrorType>(detail::ExceptionDetails(exception_).raw().type);
288 }
289
290 ///
291 /// Get the stack trace of a JavaScript exception.
292 ///
293 /// @return Returns the stack trace (an empty string for a native error, a thrown value that
294 /// isn't an error object, or once the exception's page is gone).
295 ///
296 /// @note For a thrown value that isn't an error object, this converts it to a string, which
297 /// can run script.
298 ///
299 std::string stack() const {
300 observed_ = true;
301 detail::ExceptionDetails details(exception_);
302 return detail::ExceptionDetails::Text(details.raw().stack);
303 }
304
305 ///
306 /// Get the URL of the script that created a JavaScript exception.
307 ///
308 /// @return Returns the URL (an empty string if it's unknown, for a native error, or once the
309 /// exception's page is gone).
310 ///
311 /// @note For a thrown value that isn't an error object, this converts it to a string, which
312 /// can run script.
313 ///
314 std::string source_url() const {
315 observed_ = true;
316 detail::ExceptionDetails details(exception_);
317 return detail::ExceptionDetails::Text(details.raw().source_url);
318 }
319
320 ///
321 /// Get the 1-based line where a JavaScript exception was created.
322 ///
323 /// @return Returns the line (0 if it's unknown, for a native error, or once the exception's
324 /// page is gone).
325 ///
326 /// @note For a thrown value that isn't an error object, this converts it to a string, which
327 /// can run script.
328 ///
329 unsigned line() const {
330 observed_ = true;
331 return detail::ExceptionDetails(exception_).raw().line;
332 }
333
334 ///
335 /// Get the 1-based column where a JavaScript exception was created.
336 ///
337 /// @return Returns the column (0 if it's unknown, for a native error, or once the
338 /// exception's page is gone).
339 ///
340 /// @note For a thrown value that isn't an error object, this converts it to a string, which
341 /// can run script.
342 ///
343 unsigned column() const {
344 observed_ = true;
345 return detail::ExceptionDetails(exception_).raw().column;
346 }
347
348 ///
349 /// Get the error's code (an empty string if it has none).
350 ///
351 /// For a native error, this is the code from WithCode(). For a JavaScript exception, it's the
352 /// thrown value's `code` property, if that's a string of up to 64 bytes. The errors the
353 /// library creates use the codes listed in the class notes.
354 ///
355 /// @note Reading a JavaScript exception's property can run a getter.
356 ///
357 std::string code() const {
358 observed_ = true;
359 if (kind_ == Kind::kException && exception_ && ulJSValueIsAlive(exception_)) {
360 detail::SwallowedException swallow;
361 ULJSValue prop = ulJSObjectGetProperty(exception_, "code", swallow.out());
362 if (prop) {
363 char buffer[64];
364 size_t length = 0;
365 bool ok = ulJSValueGetType(prop) == kULJSType_String
366 && ulJSValueGetUTF8(prop, buffer, sizeof(buffer), &length, swallow.out());
367 ulDestroyJSValue(prop);
368 if (ok)
369 return std::string(buffer, length);
370 }
371 }
372 return code_;
373 }
374
375 ///
376 /// Convert this error to a JavaScript value.
377 ///
378 /// For a JavaScript exception whose page is alive, you get the thrown value. Otherwise you get a
379 /// new error object with type(), message(), and the code from WithCode().
380 ///
381 /// @param ctx The context to create the value in.
382 ///
383 /// @return Returns a new handle you must destroy with ulDestroyJSValue() (NULL if `ctx` is
384 /// gone).
385 ///
386 /// @pre Must be called on the Renderer's thread.
387 ///
388 ULJSValue ToJS(ULJSContext ctx) const {
389 observed_ = true;
390 if (kind_ == Kind::kException && exception_ && ulJSValueIsAlive(exception_))
391 return ulCreateJSValueRef(exception_);
392 static_assert(static_cast<unsigned>(ErrorType::Error) == kULJSErrorType_Error
393 && static_cast<unsigned>(ErrorType::TypeError) == kULJSErrorType_TypeError
394 && static_cast<unsigned>(ErrorType::RangeError)
395 == kULJSErrorType_RangeError
396 && static_cast<unsigned>(ErrorType::SyntaxError)
397 == kULJSErrorType_SyntaxError
398 && static_cast<unsigned>(ErrorType::ReferenceError)
399 == kULJSErrorType_ReferenceError,
400 "js::ErrorType must mirror ULJSErrorType");
401 ULJSValue error = ulCreateJSError(ctx, static_cast<ULJSErrorType>(type_),
402 message().c_str());
403 if (error && !code_.empty()) {
404 detail::SwallowedException swallow;
405 ULJSValue code_value = ulCreateJSValueStringUTF8(ctx, code_.data(), code_.size());
406 ulJSObjectSetProperty(error, "code", code_value, kULJSPropertyAttributes_None,
407 swallow.out());
408 ulDestroyJSValue(code_value);
409 }
410 return error;
411 }
412
413 ///
414 /// Get a readable description of the error.
415 ///
416 /// For a JavaScript exception, this converts the thrown value to a string (which can run
417 /// script) and falls back to "JavaScript exception" if that fails.
418 ///
419 std::string message() const {
420 observed_ = true;
421 if (kind_ == Kind::kException) {
422 detail::SwallowedException swallow;
423 if (ULString s = ulJSValueToString(exception_, swallow.out())) {
424 std::string result(ulStringGetData(s), ulStringGetLength(s));
425 ulDestroyString(s);
426 return result;
427 }
428 return "A JavaScript exception was thrown.";
429 }
430 if (kind_ == Kind::kPageGone)
431 return "The page behind this handle is gone (it navigated away, its frame was removed, or "
432 "its View was destroyed).";
433 if (kind_ == Kind::kEmpty)
434 return "The handle is empty (it was never assigned, was moved from, or came from a read "
435 "that failed).";
436 return message_;
437 }
438
439 private:
440 friend struct detail::ErrorAccess;
441
442 enum class Kind { kNative, kException, kPageGone, kEmpty };
443
444 explicit Error(Kind kind) : kind_(kind) {}
445 Error(ErrorType type, std::string message)
446 : kind_(Kind::kNative), type_(type), message_(std::move(message)) {}
447
448 void ReleaseException() {
449 if (exception_ && !observed_)
450 ulJSValueReportUnobservedException(exception_);
451 ulDestroyJSValue(exception_);
452 exception_ = nullptr;
453 }
454
455 Kind kind_ = Kind::kNative;
457 ULJSValue exception_ = nullptr;
458 std::string message_;
459 std::string code_;
460 // Set by every accessor, so the destructor can tell a read error from a dropped one.
461 mutable bool observed_ = false;
462};
463
464/// \cond INTERNAL
465namespace detail {
466
467struct ErrorAccess {
468 static Error Empty() { return Error(Error::Kind::kEmpty); }
469};
470
471// The typed layer checks for an empty handle before the C call, since the C API reports an
472// empty handle and a page that's gone as the same failure.
473inline ultralight::detail::Unexpected<Error> EmptyHandleError() {
474 return ultralight::detail::Unexpected<Error>(ErrorAccess::Empty());
475}
476
477// Logs a C++ exception that escaped native code the library called. `where` names the call
478// (null when there's nothing to name); `what` is what() for a std::exception, else null.
479inline void LogNativeException(const char* where, const char* what) {
480 std::string log = std::string("C++ exception in ") + (where ? where : "native code") + ": "
481 + (what ? what : "unknown exception") + ". The library caught it and continued.";
482 ulJSAPIEmitDiagnostic(nullptr, log.c_str());
483}
484
485// The error for a C++ exception that escaped native code called from the page (a bound
486// callable, `path` naming it, or a Task body, `path` null). The page gets a generic message,
487// since what() can hold native details the page shouldn't see; the native log gets what().
488inline Error NativeExceptionError(const char* path, const char* what) {
489 LogNativeException(path, what);
490 std::string prefix = path ? std::string(path) + ": " : std::string();
491 return Error::Make(ErrorType::Error, prefix + "the native code failed");
492}
493
494} // namespace detail
495/// \endcond
496
497///
498/// Wraps an error to return it through a js::Result (like std::unexpected).
499///
500using ultralight::detail::Unexpected;
501
502///
503/// A value of type T or an error of type E (like std::expected).
504///
505/// @warning Don't call value() when has_value() is false. That's undefined behavior, since the
506/// library never throws C++ exceptions. Check first or use value_or().
507///
508template <typename T, typename E = Error>
509using Expected = ultralight::detail::Expected<T, E>;
510
511///
512/// The result of a JavaScript operation that can fail: a T or a js::Error.
513///
514/// Result is js::Expected<T, js::Error>, the library's own type with the members of
515/// std::expected (has_value(), value(), error(), value_or(), and_then(), transform(), and the
516/// rest). It's the same type in every file of your program, whichever C++ standard each file is
517/// compiled with.
518///
519template <typename T>
521
522///
523/// Get a Result's value, or a default-constructed T if it holds an error.
524///
525/// Use this where an empty value is a fine way to mark failure (every operation on an empty
526/// Value fails safely):
527///
528/// ```
529/// js::Value fn = js::OrEmpty(ctx.Evaluate("(x => x * 2)"));
530/// double n = js::OrEmpty(ctx.Evaluate("rawAdd(2, 3)")).Or(0.0);
531/// ```
532///
533/// @param result The Result to read.
534///
535/// @return Returns the value or a default-constructed T.
536///
537template <typename T>
538 requires std::is_default_constructible_v<T>
539[[nodiscard]] T OrEmpty(Result<T> result) {
540 return std::move(result).value_or(T());
541}
542
543} // namespace js
544} // namespace ultralight
struct C_JSContext * ULJSContext
Opaque handle to a JavaScript execution context (one page's script world in one View).
Definition View.h:25
An error from a JavaScript operation.
Definition Error.h:115
static Error AdoptException(ULJSValue exception)
Create an error from a JavaScript exception, taking ownership of the handle.
Definition Error.h:131
ULJSValue ToJS(ULJSContext ctx) const
Convert this error to a JavaScript value.
Definition Error.h:388
Error(const Error &other)
Copy constructor (takes its own reference to the exception, if any).
Definition Error.h:202
unsigned line() const
Get the 1-based line where a JavaScript exception was created.
Definition Error.h:329
std::string stack() const
Get the stack trace of a JavaScript exception.
Definition Error.h:299
Error(Error &&other) noexcept
Move constructor (other no longer holds the exception).
Definition Error.h:175
static Error TypeError(std::string message)
Create a native TypeError.
Definition Error.h:144
Error & operator=(const Error &other)
Copy assignment (takes its own reference to the exception, if any).
Definition Error.h:212
static Error PageGone()
Create a page-gone error (is_page_gone() returns true).
Definition Error.h:122
bool is_empty() const
Whether or not this error means the handle holds nothing (see the class notes).
Definition Error.h:258
std::string code() const
Get the error's code (an empty string if it has none).
Definition Error.h:357
ErrorType type() const
Get the error's type.
Definition Error.h:283
~Error()
Destructor (logs the exception first if nothing read it; see the class notes).
Definition Error.h:221
std::string message() const
Get a readable description of the error.
Definition Error.h:419
static Error Make(ErrorType type, std::string message)
Create a native error of any type.
Definition Error.h:168
ULJSValue exception() const
Get the JavaScript exception (NULL if is_exception() is false).
Definition Error.h:268
unsigned column() const
Get the 1-based column where a JavaScript exception was created.
Definition Error.h:343
bool is_page_gone() const
Whether or not this error means the handle's page is gone.
Definition Error.h:250
Error WithCode(std::string code) &&
Add a code that page script gets as the error's code property when it's thrown.
Definition Error.h:234
Error & operator=(Error &&other) noexcept
Move assignment (other no longer holds the exception).
Definition Error.h:185
bool is_exception() const
Whether or not this error holds a JavaScript exception.
Definition Error.h:242
static Error RangeError(std::string message)
Create a native RangeError.
Definition Error.h:155
std::string source_url() const
Get the URL of the script that created a JavaScript exception.
Definition Error.h:314
Definition StringSTL.h:166
@ Empty
No number (an Empty StyleValue; writing it removes the property).
Definition StyleValue.h:33
@ Text
A text node.
Definition Node.h:70
Type-checked JavaScript bridge between C++ and web pages.
Definition JSInterop.h:141
ultralight::detail::Expected< T, E > Expected
A value of type T or an error of type E (like std::expected).
Definition Error.h:509
T OrEmpty(Result< T > result)
Get a Result's value, or a default-constructed T if it holds an error.
Definition Error.h:539
ErrorType
The type of a JavaScript error, matching the standard error constructors.
Definition Error.h:54
@ TypeError
Definition Error.h:56
@ RangeError
Definition Error.h:57
@ Error
A plain Error, or an error type with no dedicated constant.
Definition Error.h:55
@ SyntaxError
Definition Error.h:58
@ ReferenceError
Definition Error.h:59
Expected< T, Error > Result
The result of a JavaScript operation that can fail: a T or a js::Error.
Definition Error.h:520
Root namespace for every public Ultralight type, function, and enumeration.
@ Error
Error icon.
Definition Dialogs.h:57