diff --git a/src/ble-pybricks-service/sagas.ts b/src/ble-pybricks-service/sagas.ts index f2f611b3..ece27928 100644 --- a/src/ble-pybricks-service/sagas.ts +++ b/src/ble-pybricks-service/sagas.ts @@ -12,7 +12,7 @@ import { takeEvery, } from 'typed-redux-saga/macro'; import { Action } from '../actions'; -import { hex } from '../utils'; +import { ensureError, hex } from '../utils'; import { BlePybricksServiceActionType, BlePybricksServiceCommandAction, @@ -101,7 +101,7 @@ function* decodeResponse(action: BlePybricksServiceDidNotifyEventAction): Genera ); } } catch (err) { - yield* put(eventProtocolError(err)); + yield* put(eventProtocolError(ensureError(err))); } } diff --git a/src/ble-uart/sagas.ts b/src/ble-uart/sagas.ts index acf1ab40..f2c1e8de 100644 --- a/src/ble-uart/sagas.ts +++ b/src/ble-uart/sagas.ts @@ -48,7 +48,7 @@ import { } from '../ble/actions'; import { BleConnectionState } from '../ble/reducers'; import { RootState } from '../reducers'; -import { hex } from '../utils'; +import { ensureError, hex } from '../utils'; import { BleUartActionType, BleUartWriteAction, @@ -83,7 +83,7 @@ function* writePybricksCommand( yield* call(() => char.writeValueWithoutResponse(action.value.buffer)); yield* put(didWriteCommand(action.id)); } catch (err) { - yield* put(didFailToWriteCommand(action.id, err)); + yield* put(didFailToWriteCommand(action.id, ensureError(err))); } } @@ -99,7 +99,7 @@ function* writeUart( yield* call(() => char.writeValueWithoutResponse(action.value.buffer)); yield* put(didWriteUart(action.id)); } catch (err) { - yield* put(didFailToWriteUart(action.id, err)); + yield* put(didFailToWriteUart(action.id, ensureError(err))); } } @@ -132,7 +132,9 @@ function* connect(_action: BleDeviceConnectAction): Generator { // this can happen if the use cancels the dialog yield* put(didFailToConnect({ reason: Reason.Canceled })); } else { - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put( + didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) }), + ); } return; } @@ -154,7 +156,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { server = yield* call([device.gatt, 'connect']); } catch (err) { disconnectChannel.close(); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put(didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) })); return; } @@ -172,7 +174,9 @@ function* connect(_action: BleDeviceConnectAction): Generator { if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) { yield* put(didFailToConnect({ reason: Reason.NoDeviceInfoService })); } else { - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put( + didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) }), + ); } return; } @@ -186,7 +190,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { } catch (err) { server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put(didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) })); return; } @@ -198,7 +202,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { } catch (err) { server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put(didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) })); return; } @@ -214,7 +218,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { } catch (err) { server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put(didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) })); return; } @@ -226,7 +230,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { } catch (err) { server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put(didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) })); return; } @@ -249,7 +253,9 @@ function* connect(_action: BleDeviceConnectAction): Generator { } catch (err) { server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put( + didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) }), + ); return; } @@ -273,7 +279,9 @@ function* connect(_action: BleDeviceConnectAction): Generator { if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) { yield* put(didFailToConnect({ reason: Reason.NoPybricksService })); } else { - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put( + didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) }), + ); } return; } @@ -287,7 +295,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { } catch (err) { server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put(didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) })); return; } @@ -326,7 +334,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { pybricksControlChannel.close(); server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put(didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) })); return; } @@ -349,7 +357,9 @@ function* connect(_action: BleDeviceConnectAction): Generator { if (err instanceof DOMException && err.code === DOMException.NOT_FOUND_ERR) { yield* put(didFailToConnect({ reason: Reason.NoPybricksService })); } else { - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put( + didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) }), + ); } return; } @@ -362,7 +372,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { pybricksControlChannel.close(); server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put(didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) })); return; } @@ -374,7 +384,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { pybricksControlChannel.close(); server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put(didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) })); return; } @@ -406,7 +416,7 @@ function* connect(_action: BleDeviceConnectAction): Generator { pybricksControlChannel.close(); server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect({ reason: Reason.Unknown, err })); + yield* put(didFailToConnect({ reason: Reason.Unknown, err: ensureError(err) })); return; } diff --git a/src/firmware/sagas.ts b/src/firmware/sagas.ts index dda1f939..680350e1 100644 --- a/src/firmware/sagas.ts +++ b/src/firmware/sagas.ts @@ -53,7 +53,7 @@ import { compile, } from '../mpy/actions'; import { RootState } from '../reducers'; -import { defined, hex, maybe } from '../utils'; +import { defined, ensureError, hex, maybe } from '../utils'; import { fmod, sumComplement32 } from '../utils/math'; import { isAndroid } from '../utils/os'; import { @@ -487,7 +487,7 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { yield* put(didFinish()); } catch (err) { - yield* put(didFailToFinish(FailToFinishReasonType.Unknown, err)); + yield* put(didFailToFinish(FailToFinishReasonType.Unknown, ensureError(err))); yield* disconnectAndCancel(); } } diff --git a/src/lwp3-bootloader/sagas-ble.ts b/src/lwp3-bootloader/sagas-ble.ts index f6f1d0e6..9b278f93 100644 --- a/src/lwp3-bootloader/sagas-ble.ts +++ b/src/lwp3-bootloader/sagas-ble.ts @@ -5,6 +5,7 @@ import { END, eventChannel } from 'redux-saga'; import { call, cancel, put, spawn, takeEvery, takeMaybe } from 'typed-redux-saga/macro'; +import { ensureError } from '../utils'; import { BootloaderConnectionAction, BootloaderConnectionActionType, @@ -35,7 +36,7 @@ function* write( } yield* put(didSend()); } catch (err) { - yield* put(didFailToSend(err)); + yield* put(didFailToSend(ensureError(err))); } } @@ -64,7 +65,7 @@ function* connect(_action: BootloaderConnectionAction): Generator { // this can happen if the use cancels the dialog yield* put(didFailToConnect(Reason.Canceled)); } else { - yield* put(didFailToConnect(Reason.Unknown, err)); + yield* put(didFailToConnect(Reason.Unknown, ensureError(err))); } return; } @@ -88,7 +89,7 @@ function* connect(_action: BootloaderConnectionAction): Generator { server = yield* call([device.gatt, 'connect']); } catch (err) { disconnectChannel.close(); - yield* put(didFailToConnect(Reason.Unknown, err)); + yield* put(didFailToConnect(Reason.Unknown, ensureError(err))); return; } @@ -103,7 +104,7 @@ function* connect(_action: BootloaderConnectionAction): Generator { // https://chromium-review.googlesource.com/c/chromium/src/+/2214098 yield* put(didFailToConnect(Reason.GattServiceNotFound)); } else { - yield* put(didFailToConnect(Reason.Unknown, err)); + yield* put(didFailToConnect(Reason.Unknown, ensureError(err))); } return; } @@ -117,7 +118,7 @@ function* connect(_action: BootloaderConnectionAction): Generator { } catch (err) { server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect(Reason.Unknown, err)); + yield* put(didFailToConnect(Reason.Unknown, ensureError(err))); return; } @@ -147,7 +148,7 @@ function* connect(_action: BootloaderConnectionAction): Generator { notificationChannel.close(); server.disconnect(); yield* takeMaybe(disconnectChannel); - yield* put(didFailToConnect(Reason.Unknown, err)); + yield* put(didFailToConnect(Reason.Unknown, ensureError(err))); return; } diff --git a/src/lwp3-bootloader/sagas.ts b/src/lwp3-bootloader/sagas.ts index 7f38bf88..4e7267de 100644 --- a/src/lwp3-bootloader/sagas.ts +++ b/src/lwp3-bootloader/sagas.ts @@ -12,7 +12,7 @@ import { takeEvery, } from 'typed-redux-saga/macro'; import { Action } from '../actions'; -import { hex } from '../utils'; +import { ensureError, hex } from '../utils'; import { isWindows } from '../utils/os'; import { BootloaderConnectionActionType, @@ -172,7 +172,7 @@ function* decodeResponse(action: BootloaderConnectionDidReceiveAction): Generato ); } } catch (err) { - yield* put(didError(err)); + yield* put(didError(ensureError(err))); } } diff --git a/src/settings/sagas.ts b/src/settings/sagas.ts index defd1e3a..8ed598fd 100644 --- a/src/settings/sagas.ts +++ b/src/settings/sagas.ts @@ -9,6 +9,7 @@ import { EventChannel, eventChannel } from 'redux-saga'; import { call, fork, put, select, take, takeEvery } from 'typed-redux-saga/macro'; import { AppActionType } from '../app/actions'; import { RootState } from '../reducers'; +import { ensureError } from '../utils'; import { SettingsActionType, SettingsSetBooleanAction, @@ -80,7 +81,7 @@ function* storeSetting(action: SettingsSetBooleanAction): Generator { try { localStorage.setItem(key, newValue); } catch (err) { - yield* put(didFailToSetBoolean(action.id, err)); + yield* put(didFailToSetBoolean(action.id, ensureError(err))); } // storage event is only raised when a value is changed externally, so we diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index 39dd7be5..09b19f59 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020 The Pybricks Authors -import { assert, defined, hex, maybe } from '.'; +import { assert, defined, ensureError, hex, maybe } from '.'; test('assert', () => { const assertTrue = jest.fn(() => assert(true, 'should not throw')); @@ -34,3 +34,13 @@ test('hex', () => { expect(hex(1, 4)).toBe('0x0001'); expect(hex(2, 8)).toBe('0x00000002'); }); + +test('ensureError', () => { + const err = new Error('test error'); + expect(ensureError(err)).toBe(err); + + const stringToErrorMessage = 'not an Error'; + const stringToError = expect(ensureError(stringToErrorMessage)); + stringToError.toHaveProperty('name', 'Error'); + stringToError.toHaveProperty('message', stringToErrorMessage); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index 60fee754..d85b9bda 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020 The Pybricks Authors +// Copyright (c) 2020-2021 The Pybricks Authors /** * Asserts that an assumption is true. This is used to detect programmer errors @@ -29,7 +29,7 @@ export async function maybe(promise: Promise): Promise> { try { return [await promise]; } catch (err) { - return [undefined, err]; + return [undefined, ensureError(err)]; } } @@ -41,3 +41,25 @@ export async function maybe(promise: Promise): Promise> { export function hex(n: number, pad: number): string { return `0x${n.toString(16).padStart(pad, '0')}`; } + +function isError(err: unknown): err is Error { + const maybeError = err as Error; + + return ( + maybeError !== undefined && + typeof maybeError.name === 'string' && + typeof maybeError.message === 'string' + ); +} + +export function ensureError(err: unknown): Error { + if (isError(err)) { + return err; + } + + if (typeof err === 'string') { + return new Error(err); + } + + return Error(String(err)); +}