I would say matdev's comments in his answer is not correct, at least according to my experience.
Assuming you use "Write With Response" (WRITE_TYPE_DEFAULT) rather than "Write Without Response" (WRITE_TYPE_NO_RESPONSE), the following applies:
First you call writeCharacteristic. This method performs some sanity checks, such as the characteristic you are trying to write to has the "write" property, there are no other pending GATT operations on this BluetoothGatt object, and that the object is not closed. If those sanity checks are passed, this method returns true.
The write request is then transferred over the air to the remote device's GATT server. Here the request is processed and the GATT server returns a Write Response or an Error Response with an error code, which is sent back to the Android device. When Android receives a Write Response, it will call onCharacteristicWrite with GATT_SUCCESS (0) as status. If Android instead receives an Error Response, it will call onCharacteristicWrite and set the status to the code included in the Error Response. If the GATT server e.g. returns 0x80 (Application Error) or 0x13 (Value Not Allowed), this is the status code you will receive in your callback.
If the connection drops before the response is received, Android will call the callback with some currently undocumented non-success status code.
If you instead use "Write Without Response" (WRITE_TYPE_NO_RESPONSE), the behaviour of onCharacteristicWrite changes. Now, this callback is instead used for flow control. When the local Bluetooth stack's buffers are ready to accept another packet, this is when you will get the callback. This can happen even before the packet is sent over the air.